最短路徑算法
在交通地圖上,兩地點之間的路徑通常標有長度,我們可以用加權有向來描述地圖上的交通網。加權有向圖中每條路徑都有一個路徑權值,大小為該路徑上所有邊的權值之和。本節將重點討論頂點之間最短路徑問題。在實際問題中,路徑權值還可以表示其它類型的開銷,例如兩地之間行程所需要的時間;兩任務切換所需代價等。
本節討論的最短路徑具有方向性,問題用圖的術語描述為:給定一個起始頂點s和一個結束頂點t,在圖中找出從s到t的一條最短路徑。稱s為路徑源點,t為路徑匯點。
最短路徑問題可以進一步分為單源最短路徑和全源最短路徑。
l 單源最短路徑定義為,給定起始頂點s,找出從s到圖中其它各頂點的最短路徑。求解單源最短路徑的算法主要是Dijkstra算法和Bellman-Ford算法,其中Dijkstra算法主要解決所有邊的權為非負的單源最短路徑問題,而Bellman-Ford算法可以適用於更一般的問題,圖中邊的權值可以為負。
l 全源最短路徑定義為,找出連接圖中各對頂點的最短路徑。求解全源最短路徑的算法主要有Floyd算法和Johonson算法,其中Floyd算法可以檢測圖中的負環並可以解決不包括負環的圖中的全源最短路徑問題;Johonson算法同樣也是解決不包含負環的圖的全源最短路徑問題,但是其算法效率更高。
1 基本原則
最短路徑算法具有最短路徑的最優子結構性質,也就是兩頂點之間的最短路徑包括路徑上其它頂點的最短路徑。具體描述為:對於給定的帶權圖G=(V, E),設p=<v1, v2, …,vk>是從v1到vk的最短路徑,那么對於任意i和j,1≤i≤j≤k,pij=<vi, vi+1, …, vj>為p中頂點vi到vj的子路徑,那么pij是頂點vi到vj的最短路徑。
最短路徑算法都使用了松弛(relaxation)技術。開始進行一個最短路徑算法時,只知道圖中邊和權值。隨着處理逐漸得到各對頂點的最短路徑的信息。算法會逐漸更新這些信息,每步都會檢查是否可以找到一條路徑比當前給定路徑更短。這一過程通常稱為“松弛”。
如圖為單元最短路徑算法的松弛操作。問題為求求解頂點s到圖中各頂點之間的最短路徑,用d[i]表示頂點s到頂點i的最短路徑的長度。對權值為1的邊(v, w)進行松弛,若當前到頂點v和w的最短路徑的長度分別6和8,如圖(a),則此時d[w]<d[v]+ ω(v, w),所以對d[w]的值需要減小,並且s到頂點w的最短路徑為頂點s到v的最短路徑,再經過邊(v, w),如圖(b)。

if d[w]>d[v] + ω(v, w)
{d[w]=d[v] + ω(v, w); p[w] = v;}
2 單源最短路徑
單源最短路徑定義為,給定起始頂點s,找出從s到圖中其它各頂點的最短路徑。這里我們將得到的結果稱為最短路徑樹(shortest path tree),其中樹根為起始頂點s。
2.1 Dijkstra算法
在前面章節中討論最小支撐樹時,我們討論了Prim算法:每次選擇一條邊添加到最小支撐樹MST中,這條邊連接當前MST中某個頂點和尚未在MST中的某個頂點,其權值最小。采用類似的方案可以計算最短路徑樹SPT。開始時將源點添加到SPT中,然后,每次增加一條邊來構建SPT,所取的邊總是可以給出從源點到尚未在SPT中某個定點的最短路徑。這樣,頂點按照到源點的距離由小到大逐個添加到SPT中。這種算法稱為Dijkstra算法,具體的實現跟Prim類型,分為普通實現和基於最小堆的實現。
首先,我們需要明確Dijkstra算法的適用范圍是權值非負的圖,即解決帶有非負權值的圖中的單源最短路徑問題。下面對這一屬性做簡單分析。
給定頂點s,通過Dijkstra算法得到的最短路徑樹中,從根s到樹中各頂點u的樹路徑對應着圖中從頂點s到頂點u的最短路徑。歸納證明如下:
假設當前所得到的子樹具有這一屬性,向當前子樹中添加新的頂點u,滿足:從頂點s出發,經過當前SPT中的樹路徑,並最終到達u。可以通過選擇,使得所選擇的s到u的路徑比所有滿足條件的路徑都更短。所以增加一個新的頂點將增加到達該頂點的一條最短路徑。
如果邊的權值可以為負數,那么上述證明過程將不成立,上述證明中已經假設了向當前子樹中添加新的邊時,路徑的長度不會遞減。然而在具有負權值的邊的圖中,這個假設不能滿足,因為所遇到的任何邊都可能指向子樹中的某個頂點,而且這條邊可能有一個負權值,從而會使得到達該頂點的路徑比樹路徑更短。
以下是Dijkstra算法的實現,Dijkstra算法和基於優先隊列的Dijkstra算法都在SingleSourceShortestPaths類中實現,類中包括存放每個頂點到源點的最近距離D數組和存放各個頂點的在SPT中的父節點V數組。
* Dijkstra 算法尋找圖中單源點最短路徑
* 輸入為待尋找的圖G以及源點s
* @param s 起始頂點
*/
public void Dijkstra( int s){
if(s < 0) return;
int nv = G.get_nv();
// 初始化
for( int i = 0; i < nv; i++){
D[i] = Integer.MAX_VALUE;
V[i] = -2;
// 0 沒有添加到樹中
G.setMark(i, 0);
}
// 對起點s,父節點為-1,距離為0
V[s] = -1;
D[s] = 0;
G.setMark(s, 1);
for(Edge w = G.firstEdge(s);
G.isEdge(w); w = G.nextEdge(w)){
D[G.edge_v2(w)] = G.getEdgeWt(w);
V[G.edge_v2(w)] = s;
}
/* 在其余頂點中找到與當前SPT最近的頂點v,並將
* 頂點的父節點和頂點v添加到SPT中。其中圖的
* 權值存放在節點v中。
* 循環迭代,直至所有頂點都遍歷一遍. */
while( true){
/* 獲取與當前樹距離最近的邊,其終點為最近的頂點
* 起點為最近頂點的父節點 */
Edge E = Utilities.minNextEdge(G, V);
// 如果邊為空,函數返回
if(E == null)
break;
System.out.println("ad (" + E.get_v1() +
", " + E.get_v2() +
"),\t" + G.getEdgeWt(E));
// E的終點v被訪問過了
int v = E.get_v2();
G.setMark(v, 1);
// 更新與v相連的所有邊的距離(松弛過程)
for(Edge w = G.firstEdge(v);
G.isEdge(w); w = G.nextEdge(w)){
if(D[G.edge_v2(w)] > (D[v] + G.getEdgeWt(w))){
// 更新最短距離
D[G.edge_v2(w)] = D[v] + G.getEdgeWt(w);
// 更新父節點
V[G.edge_v2(w)] = v;
System.out.println("rx (" + w.get_v1() +
", " + w.get_v2() +
"),\t" + G.getEdgeWt(w));
}
}
}
// 根據V數組建立最短路徑樹SPT
spt.addChild(s, s, new ElemItem<Double>(D[0]));
spt.setRoot(s);
int f = -1;
// 頂點標記數組,V_idx[i] == 1表示i頂點已經在SPT中,否則不再SPT中
int[] V_idx = new int[V.length];
// 初始化
for( int i = 0; i < V_idx.length; i++)V_idx[i] = 0;
// 起始頂點s已經在SPT中
V_idx[s] = 1;
while( true){
f = -1;
for( int i = 0; i < V.length; i++){
// 頂點i不在SPT中,其父頂點V[i]在SPT中,則添加到SPT中
if(V_idx[i] == 0 && V[i] >= 0
&& V_idx[V[i]] == 1 &&
spt.addChild(V[i], i, new ElemItem<Double>(D[i]))){
V_idx[i] = 1;
f = 1;
}
}
// 一次都沒有添加,結束循環
if(f == -1) break;
}
}
算法中每次從SPT之外的頂點中選擇一個頂點v,對應邊的權值最小;然后對這條邊進行松弛操作。算法迭代直至圖中所有頂點都在SPT中為止。
以圖為例,求解圖的最短路徑樹,起始頂點為頂點0。根據算法實現過程,提取圖中最短路徑數的過程如圖(a-c)。

算法初始化階段每個頂點到起始頂點s的最短路徑長度為∞。首先從起始頂點0開始,尋找相鄰頂點1和頂點5,並對其進行松弛操作。此時SPT中根節點為0,兩個(未確定)子節點為頂點1和頂點5。其中頂點0着色為灰色(賦值1),只有着色為灰色的頂點確定為SPT中頂點。
由於頂點1和頂點5對應的邊可能會在以后的操作中進行松弛操作,所以SPT中這兩個頂點是未確定的,頂點着色也未改變。

選擇與當前SPT中頂點0最近的頂點5,首先將頂點5確定為SPT中頂點0的子節點;然后對其相鄰頂點進行松弛操作。相鄰頂點為頂點1和頂點4,其中頂點4的最短距離需要更新。

選擇與當前SPT中頂點0、頂點5最近的頂點4,首先將頂點4確定為SPT中頂點5的子節點;然后對其相鄰頂點進行松弛操作。相鄰頂點為頂點2和頂點3,這兩個頂點都需要更新最短距離。
接下來將先后選擇邊(5, 1)、(4, 2)和(4, 3),並進行松弛操作。最終得到的SPT為:

將圖作為算法代碼的測試輸入,編寫示例程序如下:
public static void main(String args[]){
GraphLnk GL =
Utilities.BulidGraphLnkFromFile("Graph\\graph8.txt");
SingleSourceShortestPaths sp =
new SingleSourceShortestPaths(GL);
sp.Dijkstra(0);
sp.spt.ford_print_tree();
}
}
算法跟蹤頂點選擇和邊松弛操作的過程, 每個頂點距離起始頂點的最短距離記錄在數組D中,頂點的父節點保存在數組V中,最終利用前面章節中討論的廣義樹存放SPT。程序運行結果為:
● relax (0, 5), 29
○ found (0, 5), 29
● relax (5, 4), 21
○ found (5, 4), 21
● relax (4, 2), 32
● relax (4, 3), 36
○ found (5, 1), 29
○ found (4, 2), 32
○ found (4, 3), 36
6 節點,前序遍歷打印:
|—— 0.0(0)
|—— 41.0(1)
|—— 29.0(5)
|—— 50.0(4)
|—— 82.0(2)
|—— 86.0(3)
結果第一部分為算法將各頂點添加到SPT中以及各條邊的松弛操作。第二部分表示算法最終獲得的SPT樹。讀者可以自行對照並理解示例程序的運行結果和上面分析步驟。
在前面章節中,Prim算法可以借助於優先隊列(最小堆)來提高效率,這里也可以采用這種策略。具體算法過程其讀者自行理解,本書提供該算法的實現,具體參見程序。通過分析可以發現基於最小堆的Dijkstra算法的時間開銷與ElgV成正比。
2.2 BellmanFord算法
Bellman-Ford算法誕生於20世紀50年代,對於不包含負環的圖,該算法可以簡單有效地求解圖的單源最短路徑問題。算法的基本思路非常簡單:以任意順序考慮圖的邊,沿着各條邊進行松弛操作,重復操作|V|次(|V|表示圖中頂點的個數)。
對有向帶權圖G = (V, E),從頂點s起始,利用Bellman-Ford算法求解各頂點最短距離,算法描述如下:
for each edge(u, v) ∈ E
RELAX(u, v)
算法對每條邊做松弛操作,並且重復|V|次,所以算法可以在於|V|·|E|成正比的時間內解決單源最短路徑問題。算法十分簡單,但是在實際中並不被采用,對其做簡單的改進就可以得到更高效算法。
我們對算法的正確性做簡單分析。設每個頂點距離起始頂點s的最短距離存放在數組D中。
我們首先假設以下命題為真:算法在第i遍處理之后,對於所有頂點u,D[u]不大於s到u且包含i條(或更少)邊的最短路徑的長度。
根據以上命題,經過|V|-1次迭代后,對所給定的頂點u,D[u]為任何從s到u且包含|V|-1條(或更少)邊的最短路徑的長度的下界。此時算法可以停止迭代,因為包含|V|條邊(或更多)的任何路徑將必然有一個環,通過去除這個環將可以找到一條包含|V|-1(或更少)邊的路徑,該路徑長度不大於去環前的路徑的長度。所以D[u]同時又是從s到u的最短路徑的上界,既然D[u]同時是下界和上界,那么必然是最短路徑的長度。
下面我們對上述命題做歸納證明。i為0時,命題自然為成立;假設命題對於i成立,那么對於每個給定的頂點u分兩種情況:
l 在從s到u包含i+1條(或更少)邊的路徑中,如果其中最短路徑長度為i(或更少),那么D[u]不做調整。
l 否則,有一條從s到u且包含i+1條邊的路徑,其長度比s到u且包含i(或更少)條邊的任何路徑都短。該路徑必然由s先到達某個頂點w的路徑再加上邊(w, u)所組成。由歸納假設,D[w]是從s到w的最短距離的上邊界,而且第i+1遍處理會對各條邊進行檢查。
所以算法在第i+1遍處理之后,對於所有頂點u,D[u]不大於s到u且包含i條(或更少)邊的最短路徑的長度
然而算法每遍處理對於各條邊都進行檢查將是很大的浪費,因為有大量的邊並不會導致有效的松弛。事實上,唯一可能導致調整的邊僅為某些特定頂點出發的邊:這些頂點的值在上一遍處理中發生了變化。
那么可以對算法進行優化,即每遍處理只對特定頂點出發的邊做松弛操作。可以將發生變化的頂點的記錄下來,在下一遍處理時對一這些頂點為源點的邊做松弛操作。我們使用隊列結構來存儲這些頂點,以下是算法的實現,算法在MinusWeightGraph類中實現,類中包括存放每個頂點到源點的最近距離D數組和存放各個頂點的在SPT中的父節點V數組。
* Bellman-Ford 算法求解給定圖的單源最短路徑;
* 圖中邊的權值可以是負數。
* @param s 起始頂點
*/
public void BellmanFord( int s){
if(s < 0) return;
int nv = G.get_nv();
// 初始化
for( int i = 0; i < nv; i++){
D[i] = Double.MAX_VALUE;
V[i] = -2;
G.setMark(i, 0);
}
// 隊列Q
LinkQueue Q = new LinkQueue();
// 起始頂點的距離為0
D[s] = 0;
// 將起點s和nv添加到隊列中
int M = Integer.MAX_VALUE;
Q.enqueue( new ElemItem<Integer>(s));
Q.enqueue( new ElemItem<Integer>(M));
System.out.print("●");
Q.printQueue();
// 迭代過程,直到Q為空
while(Q.currSize() != 0){
int f = -1;
int v, N = 0;
while(M == (v = ((Integer)(Q.dequeue().elem)).intValue())){
if(N++ > nv){ f = 1; break;}
Q.enqueue( new ElemItem<Integer>(M));
}
System.out.print("⊙ ");
Q.printQueue();
if(f == 1) break;
// 對v的所有相連的邊e
for(Edge e = G.firstEdge(v);
G.isEdge(e); e = G.nextEdge(e)){
// 更新e的終點w的距離
int w = e.get_v2();
double P = D[v] + G.getEdgeWt(e);
// 如果w經過v的路徑更短,則更新w的距離
if(P < D[w]){
D[w] = P;
// 將w添加到隊列中
Q.enqueue( new ElemItem<Integer>(w));
// 將w的父節點重置為v
V[w] = v;
}
}
System.out.print("●");
Q.printQueue();
}
// 根據V數組建立最短路徑樹SPT
mst.addChild(s, s, new ElemItem<Double>(D[s]));
mst.setRoot(s);
int f = -1;
// 頂點標記數組,V_idx[i] == 1表示i頂點已經在SPT中,否則不再SPT中
int[] V_idx = new int[V.length];
// 初始化
for( int i = 0; i < V_idx.length; i++)V_idx[i] = 0;
// 起始頂點s已經在SPT中
V_idx[s] = 1;
while( true){
f = -1;
for( int i = 0; i < V.length; i++){
// 頂點i不在SPT中,其父頂點V[i]在SPT中,則添加到SPT中
if(V_idx[i] == 0 && V[i] >= 0 && V_idx[V[i]] == 1 &&
mst.addChild(V[i], i, new ElemItem<Double>(D[i]))){
V_idx[i] = 1;
f = 1;
}
}
// 一次都沒有添加,結束循環
if(f == -1) break;
}
}
算法實現過程中,用無窮大數Integer.MAX_VALUE分離隊列中兩遍處理的頂點,變量N記錄操作了幾遍,當N等於頂點個數時算法完成。算法最終廣義樹形式的SPT。
以圖為示例,起始頂點為頂點4,根據算法過程,SPT創建過程如圖(a~f),圖中記錄每遍處理后各頂點的最短距離和隊列中的頂點標號。

最終得到的SPT為:

圖 Bellman-ford算法求解得到的SPT
以圖作為示例,編寫算法示例程序:
public static void main(String args[]){
GraphLnk GL =
Utilities.BulidGraphLnkFromFile("Graph\\graph10.txt");
MinusWeightGraph MWG = new MinusWeightGraph(GL);
MWG.BellmanFord(4);
System.out.println();
MWG.mst.ford_print_tree();
}
}
●隊列的元素項從列首到列尾為:
4, 2147483647.
⊙隊列的元素項從列首到列尾為:
2147483647.
●隊列的元素項從列首到列尾為:
2147483647, 2, 3.
⊙隊列的元素項從列首到列尾為:
3, 2147483647.
●隊列的元素項從列首到列尾為:
3, 2147483647.
⊙隊列的元素項從列首到列尾為:
2147483647.
●隊列的元素項從列首到列尾為:
2147483647, 0, 5.
⊙隊列的元素項從列首到列尾為:
5, 2147483647.
●隊列的元素項從列首到列尾為:
5, 2147483647, 1.
⊙隊列的元素項從列首到列尾為:
2147483647, 1.
●隊列的元素項從列首到列尾為:
2147483647, 1, 1.
⊙隊列的元素項從列首到列尾為:
1, 2147483647.
●隊列的元素項從列首到列尾為:
1, 2147483647, 2.
⊙隊列的元素項從列首到列尾為:
2147483647, 2.
●隊列的元素項從列首到列尾為:
2147483647, 2.
⊙隊列的元素項從列首到列尾為:
2147483647.
●隊列的元素項從列首到列尾為:
2147483647.
⊙隊列為空
6 節點,前序遍歷打印:
|—— 0.0(4)
|—— 36.0(3)
|—— -2.0(5)
|—— -31.0(1)
|—— 20.0(2)
|—— 81.0(0)
3 全源最短路徑
本節中我們將討論全源最短路徑問題。可以簡單地認為全源最短路徑問題是單源最短路徑問題的推廣,即分別以每個頂點作為起始頂點,求其其余頂點到起始頂點的最短距離。例如,在有向非負權值圖的中,將每個頂點作為起始頂點,利用Dijkstra算法求解其余頂點到起始頂點的最短距離,算法的時間開銷為VElgV。
這里我們將討論的兩種算法針對更為一般的圖,圖中各條邊的權值可以為負數。第一種算法為Floyd算法,針對稠密圖,時間開銷為V3;第二種算法為Johnson算法,針對稀疏圖,該算法結合單源最短路徑算法Bellman-Ford算法和Dijkstra算法,算法時間開銷為VElogdV。兩種算法求解的都是權值可以為負數(不包含負環)的有向帶權圖。
3.1 Floyd算法
Floyd算法比較簡單,通過檢查每條邊的距離來確定該邊是否為一條更短路徑的一部分。算法實現如下:
// 待處理的圖
GraphLnk G;
// V[i][j]表示i在生成樹中的父節點
EdgeElem P[][];
// D[i]表示V[i]與i形成的邊的權值
double D[][];
// 構造函數
public AllPairsShortestPaths(GraphLnk G){
this.G = G;
// 根據G的節點數創建數組
int V = G.get_nv();
D = new double[V][V];
// 初始化
for( int i = 0; i < V; i++)
for( int j = 0; j < V; j++)
D[i][j] = Double.MAX_VALUE;
P = new EdgeElem[V][V];
for( int i = 0; i < V; i++)
for( int j = 0; j < V; j++)
if(G.isEdge(i, j)){
// 將連接邊添加到P數組中,更新D數組
P[i][j] = new EdgeElem(
new EdgeLnk(i, j, null),
G.getEdgeWt(i, j));
D[i][j] = G.getEdgeWt(i, j);
}
// 數組D對角元設為0
for( int i = 0; i < V; i++)
D[i][i] = 0.0;
// 打印中間結果
for( int i = 0; i < D.length; i++){
for( int j = 0; j < D.length; j++){
if(D[i][j] != Double.MAX_VALUE)
System.out.print(D[i][j] + "\t");
else System.out.print("∞\t");
}
System.out.println();
}
System.out.println("\n------------------------");
}
/*
* Floyd 算法,求解全部最短路徑算法 O(V^3);
* 函數沒有入參。
*/
public void Floyd(){
int V = G.get_nv();
for( int i = 0; i < V; i++){
for( int j = 0; j < V; j++){
if(P[j][i] != null){
for( int t = 0; t < V; t++){
// 更新頂點j到頂點t的距離,即D[j][t]
if(j != t &&
D[j][t] > D[j][i] + D[i][t]){
P[j][t] = P[j][i];
D[j][t] = D[j][i] + D[i][t];
// 打印中間結果
for( int i2 = 0; i2 < D.length; i2++){
for( int j2 = 0; j2 < D.length; j2++){
if(D[i2][j2] != Double.MAX_VALUE)
System.out.print(D[i2][j2] + "\t");
else System.out.print("∞\t");
}
System.out.println();
}
System.out.println("\n------------------------");
}
}
}
}
}
}
}
算法通過三重循環實現每對頂點之間的最短路徑,如圖,對頂點每個i,松弛每條邊(j, t),檢查它的距離並確定是否存在更短的路徑,並且邊(j, i)為該路徑中的邊。算法實現過程中打印顯示i變化過程中每對頂點之間的最短距離。
算法時間開銷與V3成正比。算法中用二維數組D存放每對頂點之間的最短距離,例如,D[i][j]表示頂點i到頂點j之間的最短距離;數組P存放頂點頂點的路徑,例如,P[i][j]表示頂點i到頂點j之間最短路徑中的第一條表,按圖索驥可以找到頂點i到j之間最短路徑上的每條邊。

圖 Floyd算法三重循環松弛操作
以圖(負權圖)為示例,編寫Floyd算法 測試示例程序:
public static void main(String args[]){
GraphLnk GL =
Utilities.BulidGraphLnkFromFile("Graph\\graph10.txt");
AllPairsShortestPaths APSP = new AllPairsShortestPaths(GL);
APSP.Floyd();
System.out.println("\n各頂點最短路徑:");
for( int i = 0; i < APSP.D.length; i++){
for( int j = 0; j < APSP.D.length; j++){
if(APSP.P[i][j] != null)
System.out.print(APSP.P[i][j].get_v1()
+ "-->" +APSP.P[i][j].get_v2()
+ "\t");
else
System.out.print("-----\t");
}
System.out.println();
}
}
}
0 41 ∞ ∞ ∞ 29
∞ 0 51 ∞ 32 ∞
∞ ∞ 0 50 ∞ ∞
45 ∞ ∞ 0 ∞ -38
∞ ∞ 32 36 0 ∞
∞ -29 ∞ ∞ 21 0
------------------------
== 0 ==
0 41 ∞ ∞ ∞ 29
∞ 0 51 ∞ 32 ∞
∞ ∞ 0 50 ∞ ∞
45 86 ∞ 0 ∞ -38
∞ ∞ 32 36 0 ∞
∞ -29 ∞ ∞ 21 0
------------------------
== 1 ==
0 41 92 ∞ 73 29
∞ 0 51 ∞ 32 ∞
∞ ∞ 0 50 ∞ ∞
45 86 137 0 118 -38
∞ ∞ 32 36 0 ∞
∞ -29 22 ∞ 3 0
------------------------
== 2 ==
0 41 92 142 73 29
∞ 0 51 101 32 ∞
∞ ∞ 0 50 ∞ ∞
45 86 137 0 118 -38
∞ ∞ 32 36 0 ∞
∞ -29 22 72 3 0
------------------------
== 3 ==
0 41 92 142 73 29
146 0 51 101 32 63
95 136 0 50 168 12
45 86 137 0 118 -38
81 122 32 36 0 -2
117 -29 22 72 3 0
------------------------
== 4 ==
0 41 92 109 73 29
113 0 51 68 32 30
95 136 0 50 168 12
45 86 137 0 118 -38
81 122 32 36 0 -2
84 -29 22 39 3 0
------------------------
== 5 ==
0 0 51 68 32 29
113 0 51 68 32 30
95 -17 0 50 15 12
45 -67 -16 0 -35 -38
81 -31 20 36 0 -2
84 -29 22 39 3 0
------------------------
各頂點最短路徑
----- 0-->5 0-->5 0-->5 0-->5 0-->5
1-->4 ----- 1-->2 1-->4 1-->4 1-->4
2-->3 2-->3 ----- 2-->3 2-->3 2-->3
3-->0 3-->5 3-->5 ----- 3-->5 3-->5
4-->3 4-->3 4-->3 4-->3 ----- 4-->3
5-->1 5-->1 5-->1 5-->1 5-->1 -----
結果第一部分為算法過程中記錄的每對頂點之間的最短距離,用二維數組的形式返回;第二部分為每對頂點之間最短路徑。例如,頂點0到頂點2之間的最短路徑,首先取邊(0, 5);然后需要尋找頂點5到頂點2之間最短路徑,取邊(5, 4);然后需要尋找頂點4到頂點2之間最短路徑,取邊(4, 2),到達頂點2,所以頂點0到頂點2之間的最短路徑為<v0, v5, v4, v2>,我們可以計算這條路徑的長度為ω(0, 5)+ω(5, 4)+ω(4, 2)=29+21+32=82,與距離矩陣D[0][2]相等。
3.2 Johnson算法
Johnson算法可以在O(VElgV)時間內求解每對頂點之間的最短路徑。對於稀疏圖,該算法在要好於Floyd算法。算法與Floyd算法類似,每對頂點之間的最短距離用二維數組D表示;如果圖中存在負環,算法將輸出警告信息。Johnson算法把Bellman-Ford算法和Dijkstra算法作為其子函數。
在本節一開始我們提到,如果以每個頂點作為起始頂點,用Dijkstra算法求解單源最短路徑,則可以求解全源最短路徑,算法復雜度為VElgV。但是對含有負權值的圖,Dijkstra算法將失效。Johnson算法運用了“重賦權”技術,即將原圖中每條邊的權值ω重新賦值為ω’,並且具有以下兩個性質:
l 對所有頂點對u,v,路徑p是以權值為ω的原圖的最短路徑,當且僅當路徑p也是以權值為ω’的圖的最短路徑;
l 對於所有的邊(u, v),ω’(u, v)是非負數。
重賦權后的圖可以利用Dijkstra算法求解任意兩個頂點之間的最短路徑。稍后我們將會看到,重賦值不會改變最短路徑,其處理復雜度為O(VE)。
下面我們將構造運算使得重賦權操作后得到的新的權值ω’滿足上面提及的兩個性質。
對帶權有向圖G=(V, E),邊(u, v)的權值ω(u, v),設h為頂點映射到實數域的映射函數。對圖中每條邊(u, v),定義:
ω'(u, v) = ω(u, v) + h(u) – h(v)
在這樣的構造運算可以滿足第一條性質,即如果路徑p=<v0, …, vk>是權值ω條件下頂點v0到vk的最短路徑,那么p也是新權值ω’條件下的最短路徑。用lenω(p)表示路徑p在原圖中的長度,lenω’(p)表示路徑p在重賦權后的圖中的長度,則
lenω’(p) = ω'(v0, v1) +ω'(v1, v2) + … + ω'(vk-1, vk)
= [ω(v0, v1) + h(v0) - h(v1)] + [ω(v1, v2) + h(v1) – h(v2)]
+ … + [ω(vk-1, vk) + h(vk-1) – h(vk)]
= ω(v0, v1) + ω(v1, v2) + … + ω(vk-1, vk) + h(v0) – h(vk)
= lenω(p) + h(v0) – h(vk)
所以,如果權值為ω條件下頂點v0到vk存在一條更短的路徑p*,那么對應地,在以權值為ω’的條件下,路徑p*也比路徑p更短。
再考慮第二條性質,即保證重賦權后權值非負。我們做如下的構造運算:
對給定的圖G=(V, E),,邊(u, v)的權值ω(u, v),構造一個新的圖G’=(V’, E’),其中一個新的頂點s∉V,V’=V∪{s},E’=E∪{(s, u):u∈ V},對所有的u∈V,ω(s, u)=0。G’中沒有以頂點s為終點的邊,所以,如果G中不存在負環,那么G’中也不會存在負環。
在不存在負環的前提下,定義h(u)=lenmin(s, u),即頂點s到頂點u的最短路徑,那么對所有的邊(v, u)∈V’,h(u)≦ h(v) + ω(v, u)。那么在h(u)=lenmin(s, u)的條件下,便可滿足ω'(u, v) = ω(u, v) + h(u) – h(v) ≧ 0,這樣第二條性質便可滿足。在上一節中我們討論的Bellman-Ford算法能求解無負環的單元最短路徑問題,可以用於求解h函數,其算法復雜度為O(VE)。
根據上面的討論,Johnson算法結合Bellman-Ford算法和Dijkstra算法,包括以下幾個步驟:
l 構造原圖的擴展圖G’=(V’, E’),V’=V∪{s},E’=E∪{(s, u):u∈ V};
l 在G’中以s為起始頂點應用Bellman-Ford算法,求解各頂點到頂點s的最短路徑;
l 對原圖重賦權;
l 重賦權后以圖中每個頂點為起始頂點,應用Dijkstra算法求解每對頂點之間的最短路徑;
l 由於重賦權改變了圖中路徑的長度,最后需要還原上一步驟中求得最短路徑的長度;
根據以上步驟,算法的實現如下:
double D[][];
int P[][];
GraphLnk G;
/**
* 構造函數
*/
public JohnsonAlgo(GraphLnk G) {
this.G = G;
D = new double[G.get_nv()][G.get_nv()];
P = new int[G.get_nv()][G.get_nv()];
}
public boolean Johnson(){
// 創建一個圖_G
GraphLnk _G = new GraphLnk(G.get_nv() + 1);
for( int i = 0; i < G.get_nv(); i++){
for(Edge e = G.firstEdge(i);
G.isEdge(e); e = G.nextEdge(e))
_G.setEdgeWt(e.get_v1(), e.get_v2(), G.getEdgeWt(e));
}
// 在原圖的基礎上添加一個頂點ad
int ad = _G.get_nv() - 1;
for( int i = 0; i < G.get_nv(); i++){
_G.setEdgeWt(ad, i, 0);
}
// 首先調用Bellman-Ford算法,以ad為起始點
MinusWeightGraph swg = new MinusWeightGraph(_G);
swg.BellmanFord(ad);
// h函數
int h[] = new int[G.get_nv() + 1];
System.out.println("Bellman-Ford算法結果:");
for( int i = 0; i < _G.get_nv(); i++)
System.out.print((h[i] = ( int)swg.D[i]) + "\t");
System.out.println();
for( int i = 0; i < _G.get_nv() - 1; i++)
for(Edge e = G.firstEdge(i);
G.isEdge(e); e = G.nextEdge(e))
// 檢測有沒有負環
if(h[e.get_v2()] > h[e.get_v1()] + _G.getEdgeWt(e))
{
System.out.println("圖中有負環。");
return false;
}
// 如果沒有則重賦權
else{
int u = G.edge_v1(e), v = G.edge_v2(e);
int wt = ( int) (G.getEdgeWt(e) +
h[G.edge_v1(e)] - h[G.edge_v2(e)]);
G.setEdgeWt(u, v, wt);
}
System.out.println("重賦權后的各條邊的權值:");
for( int u = 0; u < G.get_nv(); u++){
for(Edge e = G.firstEdge(u);
G.isEdge(e);
e = G.nextEdge(e)){
System.out.print(u + "-" + e.get_v2() +
" " + G.getEdgeWt(e) + "\t");
}
System.out.println();
}
// Dijkstra 算法求解每一個頂點的最短路徑樹
SingleSourceShortestPaths sssp =
new SingleSourceShortestPaths(G);
for( int i = 0; i < G.get_nv(); i++){
sssp.Dijkstra(i);
System.out.println("\n第" + i + "頂點Dijkstra結果:");
for( int j = 0; j < G.get_nv(); j++){
System.out.print(sssp.D[j] + "\t");
D[i][j] = sssp.D[j] + h[j] - h[i];
P[i][j] = sssp.V[j];
}
System.out.println();
}
return true;
}
}
根據算法描述步驟和實現代碼,以圖為例,算法的具體過程如圖(a~i):
Bellman-Ford算法求解得到各個頂點的最短路徑的長度對應這頂點的h函數的映射值,分別為:h(0)=0, h(1)=-67, h(2)=-16, h(3)=0, h(4)=-35, h(5)=-38, h(6)=0.
接下來根據各頂點的h函數值對原圖G進行重賦權操作:
ω'(u, v) = ω(u, v) + h(u) – h(v)
過程為:
ω'(0, 1) = (41) + (0) - (-67) = 108
ω'(0, 5) = (29) + (0) - (-38) = 67
ω'(1, 2) = (51) + (-67) - (-16) = 0
ω'(1, 4) = (32) + (-67) - (-35) = 0
ω'(2, 3) = (50) + (-16) - (0) = 34
ω'(3, 0) = (45) + (0) - (0) = 45
ω'(3, 5) = (-38) + (0) - (-38) = 0
ω'(4, 2) = (32) + (-35) - (-16) = 13
ω'(4, 3) = (36) + (-35) - (0) = 1
ω'(5, 1) = (-29) + (-38) - (-67) = 0
ω'(5, 4) = (21) + (-38) - (-35) = 18
得到的圖為:
各個頂點調整:
D[0][0] = (0) - (0) + (0) = 0
D[0][1] = (67) - (0) + (-67) = 0
D[0][2] = (67) - (0) + (-16) = 51
D[0][3] = (68) - (0) + (0) = 68
D[0][4] = (67) - (0) + (-35) = 32
D[0][5] = (67) - (0) + (-38) = 29
(e) 以頂點1為起始頂點,Dijkstra算法求得的在重賦權后的圖的最短路徑樹。此時的最短路徑對應着原圖中的最短路徑,但需要調整路徑長度,各個頂點調整:
D[1][0] = (46) - (-67) + (0) = 113
D[1][1] = (0) - (-67) + (-67) = 0
D[1][2] = (0) - (-67) + (-16) = 51
D[1][3] = (1) - (-67) + (0) = 68
D[1][4] = (0) - (-67) + (-35) = 32
D[1][5] = (1) - (-67) + (-38) = 30
(f) 以頂點2為起始頂點,Dijkstra算法求得的在重賦權后的圖的最短路徑樹。此時的最短路徑對應着原圖中的最短路徑,但需要調整路徑長度,各個頂點調整:
D[2][0] = (79) - (-16) + (0) = 95
D[2][1] = (34) - (-16) + (-67) = -17
D[2][2] = (0) - (-16) + (-16) = 0
D[2][3] = (34) - (-16) + (0) = 50
D[2][4] = (34) - (-16) + (-35) = 15
D[2][5] = (34) - (-16) + (-38) = 12
(i) 以頂點5為起始頂點,Dijkstra算法求得的在重賦權后的圖的最短路徑樹。此時的最短路徑對應着原圖中的最短路徑,但需要調整路徑長度,各個頂點調整:
D[5][0] = (46) - (-38) + (0) = 84
D[5][1] = (0) - (-38) + (-67) = -29
D[5][2] = (0) - (-38) + (-16) = 22
D[5][3] = (1) - (-38) + (0) = 39
D[5][4] = (0) - (-38) + (-35) = 3
D[5][5] = (0) - (-38) + (-38) = 0
算法最終得到各頂點之間的最短路徑及路徑長度為:
每對頂點之間的最短路徑長度:
0 |
1 |
2 |
3 |
4 |
5 |
|
0 |
0 |
0 |
51 |
68 |
32 |
29 |
1 |
113 |
0 |
51 |
68 |
32 |
30 |
2 |
95 |
-17 |
0 |
50 |
15 |
12 |
3 |
45 |
-67 |
-16 |
0 |
-35 |
-38 |
4 |
81 |
-31 |
20 |
36 |
0 |
-2 |
5 |
84 |
-29 |
22 |
39 |
3 |
0 |
每對頂點之間的最短路徑:
0 |
1 |
2 |
3 |
4 |
5 |
|
0 |
-1 |
5 |
1 |
4 |
1 |
0 |
1 |
3 |
-1 |
1 |
4 |
1 |
3 |
2 |
3 |
5 |
-1 |
2 |
1 |
3 |
3 |
3 |
5 |
1 |
-1 |
1 |
3 |
4 |
3 |
5 |
1 |
4 |
-1 |
3 |
5 |
3 |
5 |
1 |
4 |
1 |
-1 |
一對頂點之間的最短路徑的表中第i行第j列的表項,表示頂點i到頂點j的最短路徑中頂點j的父節點。例如,找頂點5到頂點0之間的最短路徑時,首先找到頂點3;再找頂點5到頂點3的最短路徑,找到頂點4;再找頂點5到頂點4的最短路徑,找到頂點1;再找頂點5到頂點1的最短路徑,找到頂點5,所以頂點5到頂點0的最短路徑為<v5, v1, v4, v3, v0>。
以圖為例,編寫算法程序的示例:
public static void main(String args[]){
GraphLnk g_minus =
Utilities.BulidGraphLnkFromFile("Graph\\graph10.txt");
JohnsonAlgo ja = new JohnsonAlgo(g_minus);
ja.Johnson();
System.out.println("每對頂點之間的最短路徑長度:");
for( int i = 0; i < g_minus.get_nv(); i++){
for( int j = 0; j < g_minus.get_nv(); j++){
System.out.print(( int)ja.D[i][j] + "\t");
}
System.out.println();
}
System.out.println("每對頂點之間的最短路徑:");
for( int i = 0; i < g_minus.get_nv(); i++){
for( int j = 0; j < g_minus.get_nv(); j++){
System.out.print(ja.P[i][j] + "\t");
}
System.out.println();
}
}
}
Bellman-Ford算法結果:
0 -67 -16 0 -35 -38 0
7 節點,前序遍歷打印:
|—— 0.0(6)
|—— 0.0(0)
|—— 0.0(3)
|—— -38.0(5)
|—— -67.0(1)
|—— -16.0(2)
|—— -35.0(4)
重賦權后的各條邊的權值:
0-1 108 0-5 67
1-2 0 1-4 0
2-3 34
3-0 45 3-5 0
4-2 13 4-3 1
5-1 0 5-4 18
第0頂點Dijkstra結果:
0.0 67.0 67.0 68.0 67.0 67.0
6 節點,前序遍歷打印:
|—— 0.0(0)
|—— 67.0(5)
|—— 67.0(1)
|—— 67.0(2)
|—— 67.0(4)
|—— 68.0(3)
第1頂點Dijkstra結果:
46.0 0.0 0.0 1.0 0.0 1.0
6 節點,前序遍歷打印:
|—— 0.0(1)
|—— 0.0(2)
|—— 0.0(4)
|—— 1.0(3)
|—— 1.0(5)
|—— 46.0(0)
第2頂點Dijkstra結果:
79.0 34.0 0.0 34.0 34.0 34.0
6 節點,前序遍歷打印:
|—— 0.0(2)
|—— 34.0(3)
|—— 34.0(5)
|—— 34.0(1)
|—— 34.0(4)
|—— 79.0(0)
第3頂點Dijkstra結果:
45.0 0.0 0.0 0.0 0.0 0.0
6 節點,前序遍歷打印:
|—— 0.0(3)
|—— 45.0(0)
|—— 0.0(5)
|—— 0.0(1)
|—— 0.0(2)
|—— 0.0(4)
第4頂點Dijkstra結果:
46.0 1.0 1.0 1.0 0.0 1.0
6 節點,前序遍歷打印:
|—— 0.0(4)
|—— 1.0(3)
|—— 1.0(5)
|—— 1.0(1)
|—— 1.0(2)
|—— 46.0(0)
第5頂點Dijkstra結果:
46.0 0.0 0.0 1.0 0.0 0.0
6 節點,前序遍歷打印:
|—— 0.0(5)
|—— 0.0(1)
|—— 0.0(2)
|—— 0.0(4)
|—— 1.0(3)
|—— 46.0(0)
每對頂點之間的最短路徑長度:
0 0 51 68 32 29
113 0 51 68 32 30
95 -17 0 50 15 12
45 -67 -16 0 -35 -38
81 -31 20 36 0 -2
84 -29 22 39 3 0
每對頂點之間的最短路徑:
-1 5 1 4 1 0
3 -1 1 4 1 3
3 5 -1 2 1 3
3 5 1 -1 1 3
3 5 1 4 -1 3
3 5 1 4 1 -1
結果每個部分分別對應着算法中的各個步驟,讀者可以結合算法步驟以及上面分析的算法過程對結果做進一步理解。