- package com.xh.Dijkstra;
- //這個算法用來解決無向圖中任意兩點的最短路徑
- public class ShortestDistanceOfTwoPoint_V5 {
- public static int dijkstra(int[][] W1, int start, int end) {
- boolean[] isLabel = new boolean[W1[0].length];// 是否標號
- int[] indexs = new int[W1[0].length];// 所有標號的點的下標集合,以標號的先後順序進行存儲,實際上是一個以數組表示的棧
- int i_count = -1;//棧的頂點
- int[] distance = W1[start].clone();// v0到各點的最短距離的初始值
- int index = start;// 從初始點開始
- int presentShortest = 0;//當前臨時最短距離
- indexs[++i_count] = index;// 把已經標號的下標存入下標集中
- isLabel[index] = true;
- while (i_count<W1[0].length) {
- // 第一步:標號v0,即w[0][0]找到距離v0最近的點
- int min = Integer.MAX_VALUE;
- for (int i = 0; i < distance.length; i++) {
- if (!isLabel[i] && distance[i] != -1 && i != index) {
- // 如果到這個點有邊,並且沒有被標號
- if (distance[i] < min) {
- min = distance[i];
- index = i;// 把下標改爲當前下標
- }
- }
- }
- if (index == end) {//已經找到當前點了,就結束程序
- break;
- }
- isLabel[index] = true;//對點進行標號
- indexs[++i_count] = index;// 把已經標號的下標存入下標集中
- if (W1[indexs[i_count - 1]][index] == -1
- || presentShortest + W1[indexs[i_count - 1]][index] > distance[index]) {
- // 如果兩個點沒有直接相連,或者兩個點的路徑大於最短路徑
- presentShortest = distance[index];
- } else {
- presentShortest += W1[indexs[i_count - 1]][index];
- }
- // 第二步:將distance中的距離加入vi
- for (int i = 0; i < distance.length; i++) {
- // 如果vi到那個點有邊,則v0到後面點的距離加
- if (distance[i] == -1 && W1[index][i] != -1) {// 如果以前不可達,則現在可達了
- distance[i] = presentShortest + W1[index][i];
- } else if (W1[index][i] != -1
- && presentShortest + W1[index][i] < distance[i]) {
- // 如果以前可達,但現在的路徑比以前更短,則更換成更短的路徑
- distance[i] = presentShortest + W1[index][i];
- }
- }
- }
- //如果全部點都遍歷完,則distance中存儲的是開始點到各個點的最短路徑
- return distance[end] - distance[start];
- }
- public static void main(String[] args) {
- // 建立一個權值矩陣
- int[][] W1 = { //測試數據1
- { 0, 1, 4, -1, -1, -1 },
- { 1, 0, 2, 7, 5, -1 },
- { 4, 2, 0, -1, 1, -1 },
- { -1, 7, -1, 0, 3, 2 },
- { -1, 5, 1, 3, 0, 6 },
- { -1, -1, -1, 2, 6, 0 } };
- int[][] W = { //測試數據2
- { 0, 1, 3, 4 },
- { 1, 0, 2, -1 },
- { 3, 2, 0, 5 },
- { 4, -1, 5, 0 } };
- System.out.println(dijkstra(W1, 0,4));
- }
- }
- package com.xh.Dijkstra;
- //這個程序用來求得一個圖的最短路徑矩陣
- public class ShortestDistance_V4 {
- public static int dijkstra(int[][] W1, int start, int end) {
- boolean[] isLabel = new boolean[W1[0].length];// 是否標號
- int min = Integer.MAX_VALUE;
- int[] indexs = new int[W1[0].length];// 所有標號的點的下標集合
- int i_count = -1;
- int index = start;// 從初始點開始
- int presentShortest = 0;
- int[] distance = W1[start].clone();// v0到各點的最短距離的初始值
- indexs[++i_count] = index;// 把已經標號的下標存入下標集中
- isLabel[index] = true;
- while (true) {
- // 第一步:標號v0,即w[0][0]找到距離v0最近的點
- min = Integer.MAX_VALUE;
- for (int i = 0; i < distance.length; i++) {
- if (!isLabel[i] && distance[i] != -1 && i != index) {
- // 如果到這個點有邊,並且沒有被標號
- if (distance[i] < min) {
- min = distance[i];
- index = i;// 把下標改爲當前下標
- }
- }
- }
- if (index == end) {
- break;
- }
- isLabel[index] = true;
- indexs[++i_count] = index;// 把已經標號的下標存入下標集中
- if (W1[indexs[i_count - 1]][index] == -1
- || presentShortest + W1[indexs[i_count - 1]][index] > distance[index]) {
- presentShortest = distance[index];
- } else {
- presentShortest += W1[indexs[i_count - 1]][index];
- }
- // 第二步:獎distance中的距離加入vi
- for (int i = 0; i < distance.length; i++) {
- // 如果vi到那個點有邊,則v0到後面點的距離加
- // 程序到這裏是有問題滴! 呵呵
- if (distance[i] == -1 && W1[index][i] != -1) {// 如果以前不可達,則現在可達了
- distance[i] = presentShortest + W1[index][i];
- } else if (W1[index][i] != -1
- && presentShortest + W1[index][i] < distance[i]) {
- // 如果以前可達,但現在的路徑比以前更短,則更換成更短的路徑
- distance[i] = presentShortest + W1[index][i];
- }
- }
- }
- return distance[end] - distance[start];
- }
- public static int[][] getShortestPathMatrix(int[][] W) {
- int[][] SPM = new int[W.length][W.length];
- //多次利用dijkstra算法
- for (int i = 0; i < W.length; i++) {
- for (int j = i + 1; j < W.length; j++) {
- SPM[i][j] =dijkstra(W, i, j);
- SPM[j][i] = SPM[i][j];
- }
- }
- return SPM;
- }
- public static void main(String[] args) {
- /* 頂點集:V={v1,v2,……,vn} */
- int[][] W = { { 0, 1, 3, 4 }, { 1, 0, 2, -1 }, { 3, 2, 0, 5 },
- { 4, -1, 5, 0 } };
- int[][] W1 = { { 0, 1, 4, -1, -1, -1 }, { 1, 0, 2, 7, 5, -1 },
- { 4, 2, 0, -1, 1, -1 }, { -1, 7, -1, 0, 3, 2 },
- { -1, 5, 1, 3, 0, 6 }, { -1, -1, -1, 2, 6, 0 } };// 建立一個權值矩陣
- ;// 建立一個權值矩陣
- int[][] D = getShortestPathMatrix(W1);
- //輸出最後的結果
- for (int i = 0; i < D.length; i++) {
- for (int j = 0; j < D[i].length; j++) {
- System.out.print(D[i][j] + " ");
- }
- System.out.println();
- }
- }
- }
最短路徑問題—Bellman-Ford算法
一、算法思想
1.Dijkstra算法的侷限性
上節介紹了Dijkstra算法,該算法要求網絡中各邊上得權值大於或等於0.如果有向圖中存在帶負權值的邊,則採用Dijkstra算法求解最短路徑得到的結果有可能是錯誤的。
例如,對下圖所示的有向圖,採用Dijkstra算法求得頂點v0到頂點v2的最短距離是dist[2],即v0到v2的直接路徑,長度爲5.但從v0到v2的最短路徑應該是(v0,v1,v2),其長度爲2。
如果把圖1(a)中的邊<1,2>的權值由-5改成5,則採用Dijkstra算法求解最短路徑,得到的結果是正確的(這裏不再求解)。
爲什麼當有向圖中存在帶負權值的邊時,採用Dijkstra算法求解得到的最短路徑有時是錯誤的?答案是:Dijkstra算法在利用頂點u的dist[]去遞推T集合各頂點的dist[k]值時,前提是頂點u的dist[]值時當前T集合中最短路徑長度最小的。如果圖中所有邊的權值都是正的,這樣推導是沒有問題的。但是如果有負權值的邊,這樣推導是不正確的。例如,在圖1(d)中,第1次在T集合中找到dist[]最小的是頂點2,dist[2]等於5;但是頂點0距離頂點2的最短路徑是(v0,v1,v2),長度爲2,而不是5,其中邊<1,2>是一條負權值邊。
2.Bellman-Ford算法思想
爲了能夠求解邊上帶有負權值的單源最短路徑問題,Bellman(貝爾曼)和Ford(福特)提出了從源點逐次途經其他頂點,以縮短到達終點的最短路徑長度的方法。該方法也有一個限制條件:要求圖中不能包含權值總和爲負值的迴路。
例如圖2(a)所示的有向圖中,迴路(v0,v1,v0)包括了一條具有負權值的邊,且其路徑長度爲-1。當選擇的路徑爲(v0,v1,v0,v1,v0,v1,v0,v1,…)時,路徑的長度會越來越小,這樣頂點0到頂點2的路徑長度最短可達-∞。如果存在這樣的迴路,則不能採用Bellman-Ford算法求解最短路徑。
如果有向圖中存在由帶負權值的邊組成的迴路,但迴路權值總和非負,則不影響Bellman-Ford算法的求解,如圖2(b)所示。
權值總和爲負值的迴路我們稱爲負權值迴路,在Bellman-Ford算法中判斷有向圖中是否存在負權值迴路的方法,見後面。
假設有向圖中有n個不存在負權值迴路,從頂點v1和到頂點v25如果存在最短路徑,則此路徑最多有n-1條邊。這是因爲如果路徑上得邊數超過了n-1條時,必然會重複經過一個頂點,形成迴路;而如果這個迴路的權值總和爲非負時,完全可以去掉這個迴路,使得v1到v2的最短路徑長度縮短。下面將以此爲依據,計算從源點v0到其他每個頂點u的最短路徑長度dist[u]。
Bellman-Ford算法構造一個最短路徑長度數組序列:dist1[u],dist2[u],dist3[u],…,distn-1[u]。其中:
dist1[u]爲從源點v0到終點u的只經過一條邊的最短路徑的長度,並有dist1[u]=edge[v0,u]。
dist2[u]爲從源點v0出發最多經過不構成負權值迴路的兩條邊到達終點u的最短路徑長度。
dist3[u]爲從源點v0出發最多經過不構成負權值迴路的3條邊到達終點u的最短路徑長度。
……
distn-1[u]爲從源點v0出發最多經過不構成負權值迴路的n-1條邊到達終點u的最短路徑長度。
算法的最終目的是計算出distn-1[u],爲源點v0到頂點u的最短路徑長度。
採用遞推方式計算distk[u]。
設已經求出distk-1[u],u=0,1,…,n-1,此即從源點v0最多經過不構成負權值迴路的k-1條邊到達終點u的最短路徑的長度。
從圖的鄰接矩陣可以找出各個頂點j到達頂點u的(直接邊)距離edge[j,u],計算min{distk-1[j]+edge[j,u]},可得從源點v0途經各個頂點,最多經過不構成迴路的k條邊到達終點u的最短路徑的長度。
比較distk-1[u]和min{distk-1[j]+edge[j,u]},取較小者作爲distk[u]的值。
因此Bellman-Ford算法的遞推公式(求源點v0到各頂點u的最短路徑)爲:
初始:dist1[u]=edge[v0,u],v0是源點
遞推:distk[u]=min{distk-1[u],min{distk-1[j]+edge[j,u]}}
j=0,1,...,n-1,j<>u; k=2,3,4,...,n-1
二、算法實現
Bellman-Ford算法在實現時,需要使用以下兩個數組。
(1) 使用同一個數組dist[n]來存放一系列的distk[n],其中k=1,2,...,n-1;算法結束時dist[u]數組中存放的是distn-1[u];
(2) path[n]數組含義同Dijkstra算法中的path數組。
【例題1】利用Bellman-Ford算法求解下圖(a)中頂點0到其他各頂點的最短路徑長度,並輸出對應的最短路徑。
輸入:首先輸入頂點個數n,然後輸入每條邊的數據。每條邊的數據格式爲:u v w,分別表示這條邊的起點、終點和邊上的權值。頂點序號從0開始計起。最後一行爲-1 -1 -1,表示輸入數據的結束。
樣例輸入: 樣例輸出:
7 1 0->3->2->1
0 1 6 3 0->3->2
0 2 5 5 0->3
0 3 5 0 0->3->2->1->4
1 4 -1 4 0->3->5
2 1 -2 3 0->3->2->1->4->6
2 4 1
3 2 -2
3 5 -1
4 6 3
5 6 3
-1 -1 -1
【分析】
如圖3(c)所示,k=1時,dist數組各元素的值dist[u]就是edge[0,u](見圖3(b))。在Bellman-Ford算法執行過程中,dist數組各元素的變化如圖3(c)所示。在圖3(c)中,dist[u]的值如有更新,則用粗體,紅色標明,u=1,2,3,4,5,6。以k=2,u=1加以解釋。求dist2[1]的遞推公式是:
dist2[1]=min{ dist1[1],min{dist1[j]+edge[j,1]}} j=0,2,3,4,5,6
所以,在程序中k=2時,dist[1]的值爲:
dist[1]=min{6,min{dist[0]+edge[0,1],dist[2]+edge[2,1], dist[3]+edge[3,1], dist[4]+edge[4,1],
dist[5]+edge[5,1], dist[6]+edge[6,1]}}
=min{6,min{0+6, 5+(-2), 5+∞, ∞+∞, ∞+∞, ∞+∞}}
=3
此時dist[1]的值爲從源點v0出發,經過不構成負權值迴路的兩條邊到達頂點v1的最短路徑長度,其路徑爲(v0,v2,v1)。
在Bellman-Ford算法執行過程中,path[n]數組的變化與Dijkstra算法類似,所以在圖3中沒有列出path[n]數組的變化過程。當頂點0到其他各頂點的最短路徑長度求解完畢後,如果根據path數組求解頂點0到其他各頂點vk的最短路徑?方法跟Dijkstra算法中的方法完全一樣:從path[k]開始,採用“倒向追蹤”方法,一直找到源點v0。
在下面的代碼中,bellman(v0)函數實現了求源點v0到其他各頂點的最短路徑。在主函數中調用bellman(0),則求解的是從頂點0到其他各頂點的最短路徑。另外,主函數中的shortest數組用來保存最短路徑上各個頂點的序號,其作用和上一講中Dijkstra程序代碼中的shortest數組的作用一樣。
【參考程序】
var n:longint; //頂點個數
i,j,k,u,v,w:longint;
edge:array[0..8,0..8]of longint; //鄰接矩陣,這裏的8爲頂點個數最大值,可修改
dist,path,shortest:array[0..8]of longint; //shortest數組是輸出最短路徑上的各個頂點時存放各個頂點的序號
procedure bellman(v0:longint); //求頂點v0到其他頂點的最短路徑
var i,j,k,u:longint;
begin
for i:=0 to n-1 do //初始化
begin
dist[i]:=edge[v0,i];
if (i<>v0)and(dist[i]<1000000) then path[i]:=v0 else path[i]:=-1;
end;
for k:=2 to n-1 do //從dist(1)[u]遞推出dist(2)[u],...,dist(n-1)[u]
begin
for u:=0 to n-1 do //修改每個頂點的dist[u]和path[u]
if u<>v0 then
for j:=0 to n-1 do //考慮其他每個頂點
if (edge[j,u]<1000000)and(dist[j]+edge[j,u]<dist[u]) then //頂點j到頂點u有直接路徑,且途經頂點j可以使得dist[u]縮短
begin
dist[u]:=dist[j]+edge[j,u];
path[u]:=j;
end;
end;
end;
begin
readln(n); //讀入頂點個數
while true do
begin
readln(u,v,w); //讀入邊地起點,終點和權值
if (u=-1)and(v=-1)and(w=-1) then break;
edge[u,v]:=w; //構造鄰接矩陣
end;
for i:=0 to n-1 do //設置鄰接矩陣中其他元素的值
for j:=0 to n-1 do
if i=j then edge[i,j]:=0
else if edge[i,j]=0 then edge[i,j]:=1000000;
bellman(0); //求頂點0到其他頂點的最短路徑
for i:=1 to n-1 do
begin
write(dist[i],' '); //輸出頂點0到頂點i的最短路徑長度
//下面是輸出頂點0到頂點i的最短路徑
fillchar(shortest,sizeof(shortest),0);
k:=0; //k表示shortest數組中最後一個元素的下標
shortest[k]:=i;
while path[shortest[k]]<>0 do
begin inc(k);shortest[k]:=path[shortest[k-1]];end;
inc(k);shortest[k]:=0;
for j:=k downto 1 do write(shortest[j],'->');
writeln(shortest[0]);
end;
end.
三、關於Bellman-Ford算法的進一步討論
(1)本質思想
在從dist(k-1)[]遞推到dist(k)[]的過程中,Bellman-Ford算法的本質是對每條邊<u,v>進行判斷:設邊<u,v>的權值爲w(u,v),如圖4所示,如果邊<u,v>的引入會使得dist(k-1)[v]的值再減小,則要修改dist(k-1)[v],即:如果dist(k-1)[u]+w(u,v)<dist(k-1)[v],則要講dist(k)[v]的值修改成dist(k-1)[u]+w(u,v)。這裏將修改dist(k-1)[v]的運算稱爲一次鬆弛(slack)。
按照這樣的思想,Bellman-Ford算法的遞推公式應該改爲(求源點v0到各頂點v的最短路徑):
初始:dist(0)[v]=∞,dist(0)[v0]=0,v0是源點,v<>v0
遞推:對每條邊(u,v),dist(k)[v]=min{dist(k-1)[v],dist(k-1)[u]+w(u,v)} k=1,2,3,...,n-1
理解了這點,就能理解Bellman-Ford算法的複雜度分析、Bellman-Ford算法的優化等。
(2)時間複雜度分析
在例題1的Bellman-Ford算法代碼中,有一個三重嵌套的for循環,如果使用鄰接矩陣存儲有向圖,最內層的if語句的總執行次數爲n^3,所以算法的時間複雜度爲o(n^3)。
如果使用鄰接表來存儲有向圖,內層的兩個for循環可以改成一個while循環,可以使算法的時間複雜度降爲o(n*m),其中n爲有向圖中頂點個數,m爲邊的數目。這時因爲,鄰接表裏直接存儲了邊地信息,瀏覽完所有的邊,複雜度爲o(m)。而鄰接矩陣是間接存儲邊,瀏覽完所有的邊,複雜度爲o(n^2)。
使用鄰接表存儲思想實現Bellman-Ford算法的具體過程爲:
將每條邊的信息(兩個頂點u,v和權值w,可以使用記錄record來實現)存儲到一個數組edges中,從dist(k-1)[]遞推到dist(k)[]的過程中,對edges數組中的每條邊<u,v>,判斷一下邊<u,v>的引入,是否會縮短源點v0到頂點v的最短路徑長度。
根據上面的分析,可以將例題1中的bellman函數簡化成如下代碼:
procedure bellman(v0:longint); //求頂點v0到其他頂點的最短路徑
vari,k:longint;
begin
for i:=0 to n-1 do //初始化dist數組
begin dist[i]:=1000000;path[i]:=-1; end;
dist[v0]:=0;
for k:=1 to n-1 do //從dist(0)[u]遞推出dist(1)[u],...,dist(n-1)[u]
//判斷第i條邊<u,v>的引入,是否會縮短源點v0到頂點v的最短路徑長度
for i:=0 to m-1 do //m爲邊的數目,即edges數組中元素個數
if(dist[edges[i].u]<.1000000)and(edges[i].w+dist[edges[i].u]<dist[edges[i].v])then
begin
dist[edges[i].v]:=edges[i].w+dist[edges[i,u];
path[edges[i].v]:=edges[i].u;
end;
end;
其中,dist數組各元素中,除源點v0外,其他頂點的dist[]值都初始化爲∞,這樣Bellman-Ford算法需要多遞推一次,詳見後面。
(3)Bellman-Ford算法負權值迴路的判斷方法
如果存在從源點可達的負權值迴路,則最短路徑不存在,因爲可以重複走這個迴路,使得路徑無窮小。在Bellman-Ford算法中,判斷是否存在從源點可達的負權值迴路的方法如下。在求出dist(n-1)[]後,再對每條邊<u,v>判斷一下:加入這條邊是否會使得頂點v的最短路徑值再縮短。即判斷:
dist[u]+edge[u,v]<dist[v]
是否成立,如果成立,則說明存在從源點可達的負權值迴路。代碼如下:
for i:=0 to n-1 do //採用鄰接矩陣
for j:=0 to n-1 do
if (edge[i,j]<1000000)and(dist[i]+edge[i,j]<dist[j]) then exit(0);//存在從源點可達的負權值迴路
exit(1); //不存在從源點可達的負權值迴路
或者:
for i:=0 to m-1 do
if(dist[edges[i].u]<>1000000)and(edges[i].w+dist[edges[i].u]<dist[edges[i].v])then exit(0); //存在從源點可達的負權值迴路
exit(1); //不存在從源點可達的負權值迴路
(4)Bellman-Ford算法中數組dist的初始值
dist數組的初始值爲鄰接矩陣中源點v0所在的行,實際上還可以採用以下方式對dist數組初始化:除源點v0外,其他頂點的最短距離初始爲∞(在程序實現時可以用一個權值不會達到的一個大數表示):源點dist[v0]=0。這樣Bellman-Ford算法的第1重for循環要多執行一次,即要執行n-1次;且執行第1次後,dist數組的取值爲鄰接矩陣中源點v0所在的行。
(5)Bellman-Ford算法的改進
Bellman-Ford算法是否一定要循環n-2次,n爲頂點個數,即是否一定需要從dist(1)[u]遞推到dist(n-1)[u]?
答案是未必!其實只要在某次循環過程中,考慮每條邊後,都沒能改變當前源點到所有頂點的最短路徑長度,那麼Bellman-Ford算法就可以提前結束了。