1. 程式人生 > >Floyd-Warshall演算法詳解(轉)

Floyd-Warshall演算法詳解(轉)

Floyd-Warshall演算法,簡稱Floyd演算法,用於求解任意兩點間的最短距離,時間複雜度為O(n^3)。我們平時所見的Floyd演算法的一般形式如下:

1 void Floyd(){
2     int i,j,k;
3     for(k=1;k<=n;k++)
4         for(i=1;i<=n;i++)
5             for(j=1;j<=n;j++)
6                 if(dist[i][k]+dist[k][j]<dist[i][j])
7                     dist[i][j]=dist[i][k]+dist[k][j];
8
}

注意下第6行這個地方,如果dist[i][k]或者dist[k][j]不存在,程式中用一個很大的數代替。最好寫成if(dist[i][k]!=INF && dist[k][j]!=INF && dist[i][k]+dist[k][j] < dist[i][j]),從而防止溢位所造成的錯誤。
上面這個形式的演算法其實是Floyd演算法的精簡版,而真正的Floyd演算法是一種基於DP(Dynamic Programming)的最短路徑演算法。
設圖G中n 個頂點的編號為1到n。令c [i, j, k]表示從i 到j 的最短路徑的長度,其中k 表示該路徑中的最大頂點,也就是說c[i,j,k]這條最短路徑所通過的中間頂點最大不超過k。因此,如果G中包含邊 < i, j>,則c[i, j, 0] =邊 < i, j> 的長度;若i= j ,則c[i,j,0]=0;如果G中不包含邊 < i, j>,則c (i, j, 0)= +∞。c[i, j, n] 則是從i 到j 的最短路徑的長度。
對於任意的k>0,通過分析可以得到:中間頂點不超過k 的i 到j 的最短路徑有兩種可能:該路徑含或不含中間頂點k。若不含,則該路徑長度應為c[i, j, k-1],否則長度為 c[i, k, k-1] +c [k, j, k-1]。c[i, j, k]可取兩者中的最小值。
狀態轉移方程:c[i, j, k]=min{c[i, j, k-1], c [i, k, k-1]+c [k, j, k-1]},k>0。
這樣,問題便具有了最優子結構性質,可以用動態規劃方法來求解。
這裡寫圖片描述


為了進一步理解,觀察上面這個有向圖:若k=0, 1, 2, 3,則c[1,3,k]= +∞;c[1,3,4]= 28;若k = 5, 6, 7,則c [1,3,k] = 10;若k=8, 9, 10,則c[1,3,k] = 9。因此1到3的最短路徑長度為9。
下面通過程式來分析這一DP過程,對應上面給出的有向圖:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 const int INF = 100000;
 5 int n=10,map[11][11],dist[11][11][11];
 6 void init(){
 7
int i,j; 8 for(i=1;i<=n;i++) 9 for(j=1;j<=n;j++) 10 map[i][j]=(i==j)?0:INF; 11 map[1][2]=2,map[1][4]=20,map[2][5]=1; 12 map[3][1]=3,map[4][3]=8,map[4][6]=6; 13 map[4][7]=4,map[5][3]=7,map[5][8]=3; 14 map[6][3]=1,map[7][8]=1,map[8][6]=2; 15 map[8][10]=2,map[9][7]=2,map[10][9]=1; 16 } 17 void floyd_dp(){ 18 int i,j,k; 19 for(i=1;i<=n;i++) 20 for(j=1;j<=n;j++) 21 dist[i][j][0]=map[i][j]; 22 for(k=1;k<=n;k++) 23 for(i=1;i<=n;i++) 24 for(j=1;j<=n;j++){ 25 dist[i][j][k]=dist[i][j][k-1]; 26 if(dist[i][k][k-1]+dist[k][j][k-1]<dist[i][j][k]) 27 dist[i][j][k]=dist[i][k][k-1]+dist[k][j][k-1]; 28 } 29 } 30 int main(){ 31 int k,u,v; 32 init(); 33 floyd_dp(); 34 while(cin>>u>>v,u||v){ 35 for(k=0;k<=n;k++){ 36 if(dist[u][v][k]==INF) cout<<"+∞"<<endl; 37 else cout<<dist[u][v][k]<<endl; 38 } 39 } 40 return 0; 41 }

輸入 1 3
輸出 +∞
+∞
+∞
+∞
28
10
10
10
9
9
9
Floyd-Warshall演算法不僅能求出任意2點間的最短路徑,還可以儲存最短路徑上經過的節點。下面用精簡版的Floyd演算法實現這一過程,程式中的圖依然對應上面的有向圖。

 1 #include <iostream>
 2 using namespace std;
 3 
 4 const int INF = 100000;
 5 int n=10,path[11][11],dist[11][11],map[11][11];
 6 void init(){
 7     int i,j;
 8     for(i=1;i<=n;i++)
 9         for(j=1;j<=n;j++)
10             map[i][j]=(i==j)?0:INF;
11     map[1][2]=2,map[1][4]=20,map[2][5]=1;
12     map[3][1]=3,map[4][3]=8,map[4][6]=6;
13     map[4][7]=4,map[5][3]=7,map[5][8]=3;
14     map[6][3]=1,map[7][8]=1,map[8][6]=2;
15     map[8][10]=2,map[9][7]=2,map[10][9]=1;
16 }
17 void floyd(){
18     int i,j,k;
19     for(i=1;i<=n;i++)
20         for(j=1;j<=n;j++)
21             dist[i][j]=map[i][j],path[i][j]=0;
22     for(k=1;k<=n;k++)
23         for(i=1;i<=n;i++)
24             for(j=1;j<=n;j++)
25                 if(dist[i][k]+dist[k][j]<dist[i][j])
26                     dist[i][j]=dist[i][k]+dist[k][j],path[i][j]=k;
27 }
28 void output(int i,int j){
29     if(i==j) return;
30     if(path[i][j]==0) cout<<j<<' ';
31     else{
32         output(i,path[i][j]);
33         output(path[i][j],j);
34     }
35 }
36 int main(){
37     int u,v;
38     init();
39     floyd();
40     while(cin>>u>>v,u||v){
41         if(dist[u][v]==INF) cout<<"No path"<<endl;
42         else{
43             cout<<u<<' ';
44             output(u,v);
45             cout<<endl;
46         }
47     }
48     return 0;
49 }

輸入 1 3
輸出 1 2 5 8 6 3

floyd演算法
弗洛伊德(Floyd)演算法過程:
1、用D[v][w]記錄每一對頂點的最短距離。
2、依次掃描每一個點,並以其為基點再遍歷所有每一對頂點D[][]的值,看看是否可用過該基點讓這對頂點間的距離更小。

演算法理解:
最短距離有三種情況:
1、兩點的直達距離最短。(如下圖 < v,x>)
2、兩點間只通過一箇中間點而距離最短。(圖 < v,u>)
3、兩點間用通過兩各以上的頂點而距離最短。(圖 < v,w>)

對於第一種情況:在初始化的時候就已經找出來了且以後也不會更改到。
對於第二種情況:弗洛伊德演算法的基本操作就是對於每一對頂點,遍歷所有其它頂點,看看可否通過這一個頂點讓這對頂點距離更短,也就是遍歷了圖中所有的三角形(演算法中對同一個三角形掃描了九次,原則上只用掃描三次即可,但要加入判斷,效率更低)。
對於第三種情況:如下圖的五邊形,可先找一點(比如x,使< v,u>=2),就變成了四邊形問題,再找一點(比如y,使 < u,w>=2),可變成三角形問題了(v,u,w),也就變成第二種情況了,由此對於n邊形也可以一步步轉化成四邊形三角形問題。(這裡面不用擔心哪個點要先找哪個點要後找,因為找了任一個點都可以使其變成(n-1)邊形的問題)。

這裡寫圖片描述

floyd的核心程式碼:

for (k=0;k<g.vexnum;k++)
{
    for (i=0;i<g.vexnum;i++)
    {
        for (j=0;j<g.vexnum;j++)
        {
            if (distance[i][j]>distance[i][k]+distance[k][j])
            {
                distance[i][j]=distance[i][k]+distance[k][j];
            }
        }
    }
}

結合程式碼 並參照上圖所示 我們來模擬執行下 這樣才能加深理解:
第一關鍵步驟:當k執行到x,i=v,j=u時,計算出v到u的最短路徑要通過x,此時v、u聯通了。
第二關鍵步驟:當k執行到u,i=v,j=y,此時計算出v到y的最短路徑的最短路徑為v到u,再到y(此時v到u的最短路徑上一步我們已經計算過來,直接利用上步結果)。
第三關鍵步驟:當k執行到y時,i=v,j=w,此時計算出最短路徑為v到y(此時v到y的最短路徑長在第二步我們已經計算出來了),再從y到w。
依次掃描每一點(k),並以該點作為中介點,計算出通過k點的其他任意兩點(i,j)的最短距離,這就是floyd演算法的精髓!同時也解釋了為什麼k點這個中介點要放在最外層迴圈的原因.

//多源最短路徑,floyd_warshall演算法,複雜度O(n^3)
//求出所有點對之間的最短路經,傳入圖的大小和鄰接陣
//返回各點間最短距離min[]和路徑pre[],pre[i][j]記錄i到j最短路徑上j的父結點
//可更改路權型別,路權必須非負!
#define MAXN 200
#define inf 1000000000
typedef int elem_t;

void floyd_warshall(int n,elem_t mat[][MAXN],elem_t min[][MAXN],int pre[][MAXN]){
    int i,j,k;
    for (i=0;i<n;i++)
        for (j=0;j<n;j++)
            min[i][j]=mat[i][j],pre[i][j]=(i==j)?-1:i;
    for (k=0;k<n;k++)
        for (i=0;i<n;i++)
            for (j=0;j<n;j++)
                if (min[i][k]+min[k][j]<min[i][j])
                    min[i][j]=min[i][k]+min[k][j],pre[i][j]=pre[k][j];
}