Floyd演算法學習筆記
前言
如有錯誤,歡迎各位 dalao
批評指出。
前置芝士:
1.鄰接矩陣(Floyd
要用鄰接矩陣存圖)
2.動態規劃思想(最好學過,沒學過也沒有太大影響)
1. Floyd 所解決問題的型別
我們可以發現,如 Dijkstra,SPFA,Bellman Ford
一類的最短路演算法都是解決單源點最短路問題,也就是確定了起點或者終點來求最短路的問題。但是,我們發現,這些演算法解決多源點最短路問題,也就是有多個起點和終點的最短路問題 ,的效率太低。假設有 \(n\) 個點,\(m\) 條邊。解決多源最短路時,如果用以上三種演算法來解決,都需要分別做 \(n\) 次,來求解以每個點為起點的單源最短路,時間複雜度最慢分別是 \(O(nmlogn),O(n^2m),O(n^2m)\),在稠密圖 \(m=n*(n-1)/2≈n^2\)其中最快的都需要 \(O(n^3logm)\) ,最慢的甚至是 \(O(n^4)\) ,效率太低。因此,我們今天的主角 Floyd
就因解決多源最短路問題而誕生了!
2. Floyd 的思想和基本做法
Floyd
演算法的基本思想是動態規劃。
設 \(dp_{ij}\) 表示 \(i\) 到 \(j\) 的最短距離。(有 \(n\) 個點)
首先對於這個 \(dp\) 陣列的初始化就是將輸入的邊 \(x-y\) 權值為 \(z\) (無權圖就是 \(1\)),如果圖是無向,則 \(dp_{xy}=dp_{yx}=z\) ,如果圖是有向,則 \(dp_{xy}=z\),最後將所有 \(dp_{ii}=0 (0\le i\le n)\),比較顯然,這裡不做解釋。
接著我們進行狀態轉移。顯然,我們要轉移 \(dp_{ij}\),就需要找一個點 \(k\),來進行轉移,也就是 \(dp_{ij}\gets min(dp_{ij},dp_{ik}+dp_{kj})\),其中 \(dp_{ik}+dp_{kj}\) 就表示 \(i-k\) 的最短路與 \(j-k\) 的最短路之和,其實也就相當於一個鬆弛操作。
這裡特別要注意的是:我們的 \(k\) 那一層迴圈一定要放在 \(i\) 和 \(j\) 兩層迴圈之外,因為如果放在 \(i,j\) 以內的話,你就會發現每兩個點的最短路只會被算到一次,而當你在進行狀態轉移時,你只算到一次的話,你會發現有些點在轉移時,還沒有被更新,就會出現沒有求出最短路的情況,所以,\(k\) 的迴圈要放到 \(i,j\) 的迴圈之外。
Floyd
演算法由於 \(i,j,k\) 都要列舉一層迴圈,所以時間複雜度為 \(O(n^3)\),比開頭講的三個演算法要快。
3.Floyd 程式碼實現
//n表示有n個點
memset(dp,0x3f,sizeof(dp));//賦一個極大值,並且防止在轉移時不溢位。
//接下來進行邊的初始化和dp[i][i]=0
............
//狀態轉移
for(int k=1;k<=n;++k)//列舉k
{
for(int i=1;i<=n;++i)//列舉i
{
if(i==k)//如果i,k個點相等,則不需要更新
continue;
for(int j=1;j<=n;++j)
{
if(i==j||k==j)//同第八行
continue;
dp[i][j]=min(dp[i][k]+dp[k][j],dp[i][j]);//狀態轉移。
}
}
}
4.Floyd 演算法的一些其他應用
(1).Floyd 輸出路徑
對於輸出路徑,我們可以定義一個 \(path_{ij}\) 表示 \(dp_{ij}\) 是有 \(dp_{ipath_{ij}}+dp_{path_{ij}j}\) 轉移而來。並且在一開始,我們將所有 \(path\) 陣列的元素賦一個特殊值。(假定為 \(-1\) )
顯然,要輸出 \(i-j\) 的路徑,我們可以透過遞迴來解決。
具體程式碼如下:
//賦特殊值並且做 floyd
-----------
//輸出路徑函式
void print(int i,int j)//print(a,b) 表示輸出a,b的最短路徑
{
if(path[i][j]==-1)//因為開始初始化為-1,這裡就可以避免相鄰的再次輸出
return;
print(i,path[i][j]);//前半部分
cout<<path[i][j]<<"-->";//輸出該點
print(path[i][j],j);//後半部分
}
(2).Floyd 判斷負環
在說這一個應用之前,我們先來講一下負環的定義。
負環,就是指一個圖中,存在一個環,使得這個環的權值之和為負數,這個環就是負環。
例如下圖:
可以發現,由圖中三個點組成的一個環的權值之和為負數,這就是一個簡單的負環。
一個圖中一旦存在負環,環裡的兩個點之間的最短路可以被無限更新,因為為了使得路徑長度最短,它可以一直走這個負環,無限迴圈,這個最短路徑就可以無限縮小。也就是說,一個圖一旦存在負環,它的最短路就是 \(-\infty\) ,也就相當於無解。
雖然說 SPFA Bellmanford
兩個最短路演算法可以判斷負環,但是我們還是要講一講 Floyd
演算法判斷負環的方法。
因為負環可以使得兩個點之間的最短路無限變小,所以我們可以發現,我們可以做兩次 Floyd
,第一次來更新所謂的最短路,然後再做第二次的時候,如果兩個點之間的最短路還可以被更新,就可以說明這個圖中存在負環,比較顯然。(自己想一想)。
第二次 Floyd
程式碼實現:
for(int k=1;k<=n;++k)
{
for(int i=1;i<=n;++i)
{
if(i==k)//同第一次
continue;
for(int j=1;j<=n;++j)
{
if(i==j||j==k)//同上
continue;
if(dp[i][j]>dp[i][k]+dp[k][j])//如果可以被更新,則存在負環
{
puts("No solution!");
}
}
}
}
(3).Floyd 判斷有向圖的連通性
我們知道,對於無向圖的兩個點是否聯通,我們可以運用並查集來判斷兩個點是否聯通。
但是對於一個有向圖,並查集是不能夠維護的,所以這個時候,我們就再一次請出今天的主角 Floyd
來判斷兩個點 \(i,j\) 是否連通。
我們定義 \(dp{ij}\) 表示 \(i,j\) 是否連通,若聯通則為 \(1\) ,不連通則為 \(0\) 。
顯然,我們可以根據 Floyd
一一樣的方式進行初始化。一開始除了 \(dp_{ii}\) 全部賦值為 \(0\) ,輸入邊的時候直接初始化即可。
接下來就是狀態轉移。我們可以模仿普通的 Floyd
,再找一個點 \(k\) ,如果 \(i\) 可以到達 \(k\) ,\(k\) 可以到達 \(j\) ,就可以說明 \(i\) 可以到達 \(j\) ,轉化成轉移方程就是 \(dp_{ij}|=dp_{ik}\&dp_{kj}\)。
核心程式碼:
//n表示有n個點
memset(dp,0,sizeof(dp));//賦值。
//接下來進行邊的初始化和dp[i][i]=1
............
//狀態轉移
for(int k=1;k<=n;++k)//列舉k
{
for(int i=1;i<=n;++i)//列舉i
{
if(i==k)//同上
continue;
for(int j=1;j<=n;++j)
{
if(i==j||k==j)//同上
continue;
dp[i][j]|=dp[i][k]&dp[k][j];//狀態轉移。
}
}
}
關於 Floyd
演算法的講解就到這裡了,\(see~ you\) 。