最短路徑
即找到從一個頂點到達另一個頂點的成本最小的路徑筋量。
單點最短路徑植袍。給定一幅加權(quán)有向圖和一個起點s筋遭,回答“從s到給定的目的頂點v是否存在一條有向路徑打颤?如果有,找出最短(總權(quán)重最欣焯稀)的那條路徑编饺。”
我們計劃在本節(jié)討論下列問題:
- 加權(quán)有向圖的API和實現(xiàn)以及單點最短路徑的API
- 解決邊的權(quán)重非負的最短路徑問題的經(jīng)典Dijkstra算法响驴;
- 在無環(huán)加權(quán)有向圖中解決該問題的一種快速算法透且,邊的權(quán)重甚至可以是負值
- 適用于一般情況的經(jīng)典Bellman-Ford算法,其中圖可以含有環(huán),邊的權(quán)重也可以是負值秽誊。
4.4.2 加權(quán)有向圖的數(shù)據(jù)結(jié)構(gòu)
+++
加權(quán)有向邊的數(shù)據(jù)類型
/**
* 加權(quán)有向邊的數(shù)據(jù)類型
*/
public class DirectedEdge {
private final int v; //邊的起點
private final int w; //邊的終點
private final double weight; //邊的權(quán)重
public DirectedEdge(int v, int w, double weight) {
this.v = v;
this.w = w;
this.weight = weight;
}
public double weight() {
return weight;
}
public int from() {
return v;
}
public int to() {
return w;
}
@Override
public String toString() {
return String.format("%d->%d %.2f", v, w, weight);
}
}
DirectedEdge類的實現(xiàn)比4.3節(jié)無向邊的數(shù)據(jù)類型Edge類更簡單鲸沮,因為邊的兩個端點是有區(qū)別的。用例可以使用慣用代碼
int v=e.to(),w=e.from();來訪問DirectedEdge的兩個端點锅论。
++++
加權(quán)有向圖的數(shù)據(jù)類型
++++
/**
* 加權(quán)有向圖
*/
public class EdgeWeightedDigraph {
private final int V; //頂點總數(shù)
private int E; //邊總數(shù)
private Bag<DirectedEdge>[] adj; //鄰接表
public EdgeWeightedDigraph(int V) {
this.V = V;
this.E = 0;
adj = (Bag<DirectedEdge>[]) new Bag[V];
for (int v = 0; v < V; v++) {
adj[v] = new Bag<>();
}
}
public int V() {
return V;
}
public int E() {
return E;
}
public void addEdge(DirectedEdge e) {
adj[e.from()].add(e);
E++;
}
public Iterable<DirectedEdge> adj(int v) {
return adj[v];
}
public Iterable<DirectedEdge> edges() {
Bag<DirectedEdge> bag = new Bag<>();
for (int v = 0; v < V; v++) {
for (DirectedEdge e : adj[v])
bag.add(e);
}
return bag;
}
}
它維護了一個由頂點索引的Bag對象的數(shù)組讼溺,Bag對象的內(nèi)容為DirectedEdge對象。與Digraph類一樣最易,每條邊的鄰接表中只會出現(xiàn)一次:如果一條邊從v指向w怒坯,那么它只會出現(xiàn)在v的鄰接鏈表中。這個類可以處理自環(huán)和平行邊藻懒。
下圖是用EdgeWeightedDigraph表示左側(cè)的加權(quán)有向圖構(gòu)造出的數(shù)據(jù)結(jié)構(gòu)剔猿。
最短路徑的API
4.4.2.3 最短路徑的數(shù)據(jù)結(jié)構(gòu)
- 最短路徑樹中的邊。和深度優(yōu)先搜索束析、廣度優(yōu)先搜索和Prim算法一樣艳馒,使用一個由頂點索引的DirectedEdge對象的父鏈接數(shù)組edgeTo[],其中edgeTo[v]的值為樹中連接v和它的父節(jié)點的邊(也是從s到v的最短路徑上的最后一條邊)员寇。
- 到達起點的距離弄慰。我們需要一個由頂點索引的數(shù)組distTo[],其中distTo[v]為從s到v的已知最短路徑的長度。
我們約定蝶锋,edgesTo[s]的值為null陆爽,distTo[s]的值為0,同時約定扳缕,從起點到不可達的頂點的距離均為Double.POSITIVE_INFINITY慌闭。
4.4.2.4 邊的松弛
我們的最短路徑API的實現(xiàn)基于一個被稱為松弛的簡單操作.一開始我們只知道圖的邊和它們的權(quán)重,distTo[]中只有起點所對應(yīng)的元素值為0,其余元素的值均被初始化為Double.POSITIVE_INFINITY躯舔。隨著算法的執(zhí)行驴剔,它將起點到其他頂點的最短路徑信息存入了edgeTo[]和distTo[]數(shù)組中。在遇到新的邊的時候粥庄,通過更新這些信息就可以得到新的最短路徑丧失。
我們在其中會用到邊的松弛技術(shù),定義如下:放松邊v->w意味著檢查從s到w的最短路徑是否先從s到v惜互,然后再由v到w布讹。如果是,則根據(jù)這個情況更新數(shù)據(jù)結(jié)構(gòu)的內(nèi)容训堆。下面代碼實現(xiàn)了這個操作描验。由v到w的最短路徑是distTo[v]與e.weight()之和——如果這個值不小于distTo[w],則稱這條邊失效了并忽略坑鱼;如果這個值更小膘流,則更新數(shù)據(jù)。
4.4.2.5 頂點的松弛
實際上,實現(xiàn)會放松從一個給定頂點只會的所有邊睡扬,如下面(被重載的)relax()的實現(xiàn)所示盟蚣。注意,從任意distTo[v]為有限值的頂點v指向任意distTo[]為無窮的頂點都是有效的卖怜。如果v被放松屎开,那么這些有效邊就會被添加到edgeTo[]中。某條從起點指出的邊將會是第一條被加入edgeTo[]中的邊马靠。算法會謹慎選擇頂點奄抽,使得每次頂點松弛操作都能得出到達某個頂點的最短的路徑,最后逐漸找出到達每個頂點的最短路徑甩鳄。
4.4.2.6 為用例準備的查詢方法
edgeTo[]和distTo[]直接支持pathTo()逞度、hasPathTo()和distTo()的查詢方法。默認所有最短路徑的實現(xiàn)都包含這段代碼妙啃。前面已經(jīng)提到過档泽,只有v是從s可達的情況下,distTo[v]才是有意義的揖赴,還約定馆匿,對于從s不可達的頂點,distTo()方法都應(yīng)該返回?zé)o窮大燥滑。在實現(xiàn)這個約定時渐北,將distTo[]中所有元素都初始化為Double.POSITIVE_INFINITY,distTo[s]則為0。最短路徑算法會將從起點可達的頂點v的distTo[v]設(shè)為一個有限制铭拧,這樣就不必再用marked[]數(shù)組來在圖的搜索中標記可達的頂點赃蛛,而是檢查distTo[v]是否為Double.POSITIVE_INFINITY來實現(xiàn)hasPathTo(v)。對于pathTo()方法搀菩,我們約定如果v不是從起點科大的則返回null呕臂,如果v等于起點返回一條不含任何邊的路徑
4.4.3最短路徑算法的理論基礎(chǔ)
4.4.3.1 最優(yōu)性條件
一下命題證明了判斷路徑是否最短路徑的全局條件與放松一條邊的時候檢測的局部條件是否等價。
4.4.3.3 通用算法
由最優(yōu)性條件可以得到一個涵蓋所有最短路徑的通用算法》景希現(xiàn)在诵闭,我們暫時只研究非負權(quán)重的情況。
通用算法沒有指定邊的放松順序澎嚣。
Dijkstra算法
Dijkstra首先將distTo[s]初始化為0,distTo[]中的其他元素初始化為正無窮瘟芝。然后將distTo[]最小的非樹頂點放松并加入樹中易桃,知道所有的頂點都在樹中或所有的非樹頂點的distTo[]值為無窮大
4.4.4.1 數(shù)據(jù)結(jié)構(gòu)
要實現(xiàn)Dijkstra算法,除了distTo[]和edgeTo[]外锌俱,還需要一條索引優(yōu)先隊列pq晤郑,保存需要放松的頂點并確認下一個被放松的頂點。
代碼實現(xiàn):
public class DijkstraSP {
private DirectedEdge[] edgeTo;
private double[] distTo;
private IndexMinPQ<Double> pq;
public DijkstraSP(EdgeWeightedDigraph G, int s) {
edgeTo = new DirectedEdge[G.V()];
distTo = new double[G.V()];
pq = new IndexMinPQ<>(G.V());
for (int i = 0; i < G.V(); i++) {
distTo[i] = Double.POSITIVE_INFINITY;
}
distTo[s] = 0.0;
pq.insert(s, 0.0);
while (!pq.isEmpty())
relax(G, pq.delMin());
}
private void relax(EdgeWeightedDigraph G, int v) {
for (DirectedEdge e : G.adj(v)) {
int w = e.to();
if (distTo[w] > distTo[v] + e.weight()) {
distTo[w] = distTo[v] + e.weight();
edgeTo[w] = e;
if (pq.contains(w)) pq.change(w, distTo[w]);
else pq.insert(w, distTo[w]);
}
}
}
public double distTo(int v) {
return distTo[v];
}
public boolean hasPathTo(int v) {
return distTo[v] < Double.POSITIVE_INFINITY;
}
public Iterable<DirectedEdge> pathTo(int v) {
if (!hasPathTo(v)) return null;
Stack<DirectedEdge> path = new Stack<>();
for (DirectedEdge e = edgeTo[v]; e != null; e = edgeTo[e.from()]) {
path.push(e);
}
return path;
}
}
4.4.5 無環(huán)加權(quán)有向圖的最短路徑算法
許多應(yīng)用中的加權(quán)有向圖都是不含有有向環(huán)的,我們現(xiàn)在來學(xué)習(xí)一種比Dijkstra算法更快造寝、更簡單的在無環(huán)加權(quán)有向圖中找出最短路徑的算法磕洪,它的特點是
- 能夠在線性時間內(nèi)解決單點最短路徑問題
- 能夠處理負權(quán)重的邊
- 能夠解決相關(guān)的問題,例如找出最長路徑诫龙。
這些算法都是無環(huán)有向圖的拓撲排序算法的簡單擴展析显。
特別的是,只要將頂點的放松和拓撲排序結(jié)合起來签赃,馬上就能得到一種解決無環(huán)加權(quán)有向圖中最短路徑問題的算法谷异。
首先,將distTo[s]初始化為0锦聊,其他distTo[]元素初始化為無窮大歹嘹,然后一個個按照拓撲排序順序放松所有頂點
算法 無環(huán)加權(quán)有向圖的最短路徑算法
/**
* 無環(huán)加權(quán)有向圖的最短路徑算法
*/
public class AcyclicSp {
private DirectedEdge[] edgeTo;
private double[] distTo;
public AcyclicSp(EdgeWeightedDigraph G, int s) {
edgeTo = new DirectedEdge[G.V()];
distTo = new double[G.V()];
for (int v = 0; v < G.V(); v++) {
distTo[v] = Double.POSITIVE_INFINITY;
}
distTo[s] = 0.0;
Topological top = new Topological(G);
for (int v : top.order()) {
relax(G, v);
}
}
private void relax(EdgeWeightedDigraph G, int v) {
for (DirectedEdge e : G.adj(v)) {
int w = e.to();
if (distTo[w] > distTo[v] + e.weight()) {
distTo[w] = distTo[v] + e.weight();
edgeTo[w] = e;
}
}
}
public double distTo(int v) {
return distTo[v];
}
public boolean hasPathTo(int v) {
return distTo[v] < Double.POSITIVE_INFINITY;
}
public Iterable<DirectedEdge> pathTo(int v) {
if (!hasPathTo(v)) return null;
Stack<DirectedEdge> stack = new Stack<>();
for (DirectedEdge e = edgeTo[v]; e != null; e = edgeTo[e.from()]) {
stack.push(e);
}
return stack;
}
}
4.4.5.1 最長路徑
無環(huán)加權(quán)有向圖中的單點最長路徑。給定一幅無環(huán)加權(quán)有向圖(邊的權(quán)重可為負)和一個起點s孔庭,回答“是否存在一條從s到給定的頂點v的路徑尺上?如果有,找出最長的那條路徑”
實現(xiàn)該類的一個簡單方法是修改AcyclicSP圆到,將distTo[]的初始值變?yōu)镈ouble.NEGATIVE_INFINITY并改變relax()不等式的方向
/**
* 無環(huán)加權(quán)有向圖的最長路徑算法
*/
public class AcyclicLP {
private DirectedEdge[] edgeTo;
private double[] distTo;
public AcyclicLP(EdgeWeightedDigraph G, int s) {
edgeTo = new DirectedEdge[G.V()];
distTo = new double[G.V()];
for (int v = 0; v < G.V(); v++) {
distTo[v] = Double.NEGATIVE_INFINITY;
}
distTo[s] = 0.0;
Topological top = new Topological(G);
for (int v : top.order()) {
relax(G, v);
}
}
private void relax(EdgeWeightedDigraph G, int v) {
for (DirectedEdge e : G.adj(v)) {
int w = e.to();
if (distTo[w] < distTo[v] + e.weight()) {
distTo[w] = distTo[v] + e.weight();
edgeTo[w] = e;
}
}
}
public double distTo(int v) {
return distTo[v];
}
public boolean hasPathTo(int v) {
return distTo[v] > Double.NEGATIVE_INFINITY;
}
public Iterable<DirectedEdge> pathTo(int v) {
if (!hasPathTo(v)) return null;
Stack<DirectedEdge> stack = new Stack<>();
for (DirectedEdge e = edgeTo[v]; e != null; e = edgeTo[e.from()]) {
stack.push(e);
}
return stack;
}
}
4.4.6 一般加權(quán)有向圖的最短路徑問題
4.4.6.3 負權(quán)重的環(huán)
我們研究含有負權(quán)重邊的有向圖的時候怎抛,如果該圖含有一個權(quán)重為負的環(huán),那么最短路徑的概念就失去意義了构资。例如圖4.4.20抽诉,除了
5->4的權(quán)重為-0.66外,它和第一個示例完全相同吐绵。這里迹淌,環(huán)4->7->5->4權(quán)重為0.37+0.28-0.66=-0.01
只要圍著這個環(huán)兜圈子就能得到權(quán)重任意短的路徑
定義:加權(quán)有向圖的**負權(quán)重環(huán)**是一個總權(quán)重(環(huán)上所有邊的權(quán)重之和)為負的有向環(huán)
無論是否存在負權(quán)重環(huán),從s到可達的其他頂點的一條最短路徑都是存在的己单。不幸的是唉窃,解決這個問題的最好方法在最壞情況下所需的時間是指數(shù)級別的。一般來說纹笼,我們認為這個問題“太難了”纹份。
現(xiàn)在我們解決下列問題:
- 負權(quán)重環(huán)的檢測。給定的加權(quán)有向圖中含有負權(quán)重環(huán)嗎廷痘? 如果有蔓涧,找到它。
- 負權(quán)重環(huán)不可達時的單點最短路徑笋额。給定一幅加權(quán)有向圖和一個起點s且s無法到達任何負權(quán)重環(huán)元暴,回答“是否存在一條從s到給定頂點v的有向路徑?如果有兄猩,找出最短(總權(quán)重最熊哉怠)的那條路徑”鉴未。
總結(jié)盡管在含有環(huán)的有向圖中最短路徑?jīng)]有意義,而且也無法解決在這種有向圖中高效找出最短簡單路徑的問題鸠姨,但在實際應(yīng)用中仍需要識別出其中的負權(quán)重環(huán)铜秆。
我們不會仔細研究這個版本,因為它總是放松VE條邊且只需要稍加修改即可使算法在一般的應(yīng)用場景中更高效讶迁。
4.4.6.5 基于隊列的Bellman-Ford算法
根據(jù)經(jīng)驗我們知道在任意一輪中許多邊的放松都不會成功:只有上一輪中的distTo[]值發(fā)生變化的頂點指出的邊才能夠改變其他distTo[]元素的值连茧。
為了記錄這樣的頂點,我們使用了一條FIFO隊列添瓷。
4.4.6.6 實現(xiàn)
根據(jù)這些描述實現(xiàn)Bellman-Ford算法需要的代碼非常少
基于下面兩種數(shù)據(jù)結(jié)構(gòu):
- 一條用來保存即將被放松的頂點隊列queue梅屉;
- 一個由頂點索引的boolean的數(shù)據(jù)onQ[],用來指示頂點是否存在隊列中鳞贷,以防止將頂點重復(fù)插入隊列坯汤。
這些數(shù)據(jù)結(jié)構(gòu)保證,隊列中不出現(xiàn)重復(fù)的頂點搀愧,在某一輪中改變了edgeTo[]和distTo[]的所有頂點在下一輪中被處理惰聂。
算法 基于隊列的Bellman-Ford算法
/**
* 基于隊列的Bellman-Ford算法
*/
public class BellmanFordSP {
private double[] distTo; //從起點到某個頂點的路徑長度
private DirectedEdge[] edgeTo; //從起點到某個頂點的最后一條邊
private boolean[] onQ; //該頂點是否存在于隊列中
private Queue<Integer> queue; //正在被放松的頂點
private int cost; //relax()的調(diào)用次數(shù)
private Iterable<DirectedEdge> cycle;//edgeTo[]中是否有負權(quán)重環(huán)
public BellmanFordSP(EdgeWeightedDigraph G, int s) {
distTo = new double[G.V()];
edgeTo = new DirectedEdge[G.V()];
onQ = new boolean[G.V()];
queue = new ArrayDeque<>();
for (int v = 0; v < G.V(); v++) {
distTo[v] = Double.POSITIVE_INFINITY;
}
distTo[s] = 0.0;
queue.add(s);
onQ[s] = true;
while (!queue.isEmpty() && !hasNegativeCycle()) {
int v = queue.remove();
onQ[v] = false;
relax(G, v);
}
}
private void relax(EdgeWeightedDigraph G, int v) {
for (DirectedEdge e : G.adj(v)) {
int w = e.to();
if (distTo[w] > distTo[v] + e.weight()) {
distTo[w] = distTo[v] + e.weight();
edgeTo[w] = e;
if (!onQ[w]) {
queue.add(w);
onQ[w] = true;
}
}
if (cost++ % G.V() == 0) {
findNegativeCycle();
}
}
}
private void findNegativeCycle() {
int V = edgeTo.length;
EdgeWeightedDigraph spt;
spt = new EdgeWeightedDigraph(V);
for (int v = 0; v < V; v++) {
if (edgeTo[v] != null)
spt.addEdge(edgeTo[v]);
}
EdgeWeightedCycleFinder cf;
cf = new EdgeWeightedCycleFinder(spt);
cycle = cf.cycle;
}
private boolean hasNegativeCycle() {
return cycle != null;
}
}
public Iterable<DirectedEdge> negativeCycle(){
return cycle;
}
4.4.6.7 負權(quán)重的邊
4.4.6.8 負權(quán)重環(huán)的檢測
檢測負權(quán)重環(huán)來避免無限的循環(huán)。
- 添加一個變量cycle和一個私有函數(shù)findNegativeCycle()咱筛。如果找到負權(quán)重環(huán)搓幌,則會將cycle設(shè)為含有環(huán)中所有邊的一個迭代器。
- 每調(diào)用V次relax()方法后即調(diào)用findNegativeCycle()方法迅箩。