Floyd 算法
簡介
Floyd 算法又稱為插點(diǎn)法,是一種利用動態(tài)規(guī)劃的思想尋找給定的加權(quán)圖中多源點(diǎn)之間最短路徑的算法,與 Dijkstra 算法類似。
該算法名稱以創(chuàng)始人之一、1978 年圖靈獎獲得者、斯坦福大學(xué)計(jì)算機(jī)科學(xué)系教授羅伯特 · 弗洛伊德命名。
核心思路
路徑矩陣
- 通過一個圖的權(quán)值矩陣求出它的每兩點(diǎn)間的最短路徑矩陣。
- 從圖的帶權(quán)鄰接矩陣 A=[a(i,j)] n×n 開始,遞歸地進(jìn)行 n 次更新,即由矩陣 D(0)=A,按一個公式,構(gòu)造出矩陣 D(1);又用同樣地公式由 D(1) 構(gòu)造出 D(2);……;最后又用同樣的公式由 D(n-1) 構(gòu)造出矩陣 D(n)。矩陣 D(n) 的 i 行 j 列元素便是 i 號頂點(diǎn)到 j 號頂點(diǎn)的最短路徑長度,稱 D(n) 為圖的距離矩陣,同時還可引入一個后繼節(jié)點(diǎn)矩陣 path 來記錄兩點(diǎn)間的最短路徑。
- 采用松弛技術(shù)(松弛操作),對在 i 和 j 之間的所有其他點(diǎn)進(jìn)行一次松弛。所以時間復(fù)雜度為 O(n^3)。
狀態(tài)轉(zhuǎn)移方程
path[i,j]:=min{path[i,k]+path[k,j],path[i,j]}
算法描述
從任意一條單邊路徑開始。所有兩點(diǎn)之間的距離是邊的權(quán),如果兩點(diǎn)之間沒有邊相連,則權(quán)為無窮大。
對于每一對頂點(diǎn) u 和 v,看看是否存在一個頂點(diǎn) w (一般稱為中間點(diǎn))使得從 u 到 w 再到 v 比已知的路徑更短。如果是,更新它(專業(yè)術(shù)語為:松弛)。
遍歷直到結(jié)束,這時候存儲圖的數(shù)據(jù)結(jié)構(gòu)就得到了多源最短路徑。
優(yōu)缺點(diǎn)分析
-
Floyd 算法適用于 APSP(All Pairs Shortest Paths,多源最短路徑),是一種動態(tài)規(guī)劃算法,稠密圖效果最佳,邊權(quán)可正可負(fù)。此算法簡單有效,由于三重循環(huán)結(jié)構(gòu)緊湊,對于稠密圖,效率要高于執(zhí)行 | V | 次 Dijkstra 算法,也要高于執(zhí)行 | V | 次 SPFA 算法。
稠密圖定義:邊的條數(shù) | E | 接近 | V|2,稱為稠密圖(dense graph)
優(yōu)點(diǎn):容易理解,可以算出任意兩個節(jié)點(diǎn)之間的最短距離,代碼編寫簡單。
缺點(diǎn):時間復(fù)雜度比較高,不適合計(jì)算大量數(shù)據(jù)。時間復(fù)雜度 O(n^3),空間復(fù)雜度 O(n^2)。
Python 代碼實(shí)現(xiàn)
-
代碼:
import copy M=1000000 def Floyd(G): n=len(G) path=copy.deepcopy(G) for k in range(0,n): for i in range(0,n): for j in range(0,n): print("Comparing path[%s][%s] and {path[%s][%s]+path[%s][%s]}"%(i,j,i,k,k,j)) print("Former path[%s][%s] = %s"%(i,j,path[i][j])) path[i][j]=min(path[i][j],path[i][k]+path[k][j]) print("Present path[%s][%s] = %s\n"%(i,j,path[i][j])) return path if __name__=='__main__': G=[ [0,30,15,M,M,M], [5,0,M,M,20,30], [M,10,0,M,M,15], [M,M,M,0,M,M], [M,M,M,10,0,M], [M,M,M,30,10,0] ] print("---------------Floyd----------------") path=Floyd(G) print("Graph = ") for i in range(0,len(G)): print (path[i])
代碼詳解
引入 copy ,定義無窮值,在 Python 里還有種寫法:
M = float('inf')定義 Floyd 函數(shù),參數(shù)為圖的鄰接矩陣 G,在里面首先利用 copy 函數(shù)復(fù)制一份,不復(fù)制的話,會導(dǎo)致兩個變量指向同一個地址(鄰接矩陣)
三重 for 循環(huán),很容易理解
-
if __name__=='__main__'不理解的復(fù)制這段代碼,百度谷歌
測試
還是拿我的另一篇文章 Dijkstra 算法 中的測試用例
-
鄰接矩陣:
G=[ [0,30,15,M,M,M], [5,0,M,M,20,30], [M,10,0,M,M,15], [M,M,M,0,M,M], [M,M,M,10,0,M], [M,M,M,30,10,0] ] -
表示的圖如下:
test -
輸出:
floydtest
小結(jié)
從上面的輸出結(jié)果可以看到 G 矩陣已經(jīng)被更新了
有些走不通的路徑現(xiàn)在可以走通了,因?yàn)樵瓉硎青徑泳仃嚕簝蓚€頂點(diǎn)有鄰接邊,鄰接矩陣對應(yīng)點(diǎn)才有值;現(xiàn)在是整個網(wǎng)絡(luò),頂點(diǎn) i 到頂點(diǎn) j 如果可達(dá),那么
G[i][j]就有值。但是,我們是得到了兩個點(diǎn)的最小 cost,最短路徑要怎么得到呢?可以再借助一個 path 矩陣,它記錄了中間點(diǎn)的信息,我們可以在最后根據(jù)更新后的 G 矩陣回溯 path 矩陣得到最短路徑。
全局最短路徑的數(shù)據(jù)結(jié)構(gòu)
ShortestPath_dict = {
0: {1: [0, 2, 1], 2: [0, 2], 3: [0, 2, 5, 4, 3], 4: [0, 2, 5, 4], 5: [0, 2, 5]},
1: {0: [1, 0], 2: [1, 0, 2], 3: [1, 4, 3], 4: [1, 4], 5: [1, 5]}, 2: {0: [2, 1, 0],
1: [2, 1], 3: [2, 5, 4, 3], 4: [2, 5, 4], 5: [2, 5]},
3: {0: [], 1: [], 2: [], 4: [], 5: []},
4: {0: [], 1: [], 2: [], 3: [4, 3], 5: []},
5: {0: [], 1: [], 2: [], 3: [5, 4, 3], 4: [5, 4]}
}
- 這個數(shù)據(jù)結(jié)構(gòu)利用了字典與列表,其中每兩個頂點(diǎn)的最短路徑用列表表示,如頂點(diǎn) 0 到頂點(diǎn) 1 :
0: {1: [0, 2, 1],其中的[0, 2, 1]就是最短路徑。
利用 Floyd 得到全局最短路徑
-
代碼:
# ------------------函數(shù)------------------- def back_path(path,i,j,shortestPath): #遞歸回溯 print ("path[%s][%s] = "%(i,j),path[i][j]) if -1 != path[i][j]: shortestPath = back_path(path,i,path[i][j],shortestPath) shortestPath = back_path(path,path[i][j],j,shortestPath) if j not in shortestPath: shortestPath.append(j) return shortestPath def getShortestPath(graph,path,i,j): shortestPath = [] if graph[i][j] == float('inf') or i == j: print("頂點(diǎn)%s 不能到達(dá) 頂點(diǎn)%s!"%(i,j)) return shortestPath elif path[i][j] == -1: shortestPath.append(i) shortestPath.append(j) else : shortestPath.append(i) shortestPath = back_path(path,i,j,shortestPath) print("頂點(diǎn)%s 到 頂點(diǎn)%s 的路徑為:"%(i,j),shortestPath) return shortestPath def getAllShortestPath(graph,path): print("------正在生成全局最短路徑------") ShortestPath_dict = {} for i in range(N): ShortestPath_dict[i] = {} for j in range(N): print("嘗試生成頂點(diǎn)%s到頂點(diǎn)%s的最短路徑..."%(i,j)) if i !=j : shortestPath = getShortestPath(graph,path,i,j) ShortestPath_dict[i][j] = shortestPath print("--------------------------------") return ShortestPath_dict # ----------------------定義-------------------- M=float('inf') #無窮大 graph = [ [0,30,15,M,M,M], [5,0,M,M,20,30], [M,10,0,M,M,15], [M,M,M,0,M,M], [M,M,M,10,0,M], [M,M,M,30,10,0] ] N = len(graph) path = [] for i in range(N): path.append([]) for j in range(N): path[i].append(-1) print ("Original Graph:\n",graph) # -----------------Floyd Algorithm---------------- for k in range(N): for i in range(N): for j in range(N): if graph[i][j] > graph[i][k] + graph[k][j]: graph[i][j] = graph[i][k] + graph[k][j] path[i][j] = k print ("Shortest Graph:\n",graph) print ("Path:\n",path) print("ShortestPath =\n",getAllShortestPath(graph,path))
測試2
鄰接矩陣如上所示
表示的圖也如上所示
-
截圖
test3
last
代碼詳解2
在三重循環(huán)階段,與前文第一種做法不同的是:每次還加入了
path[i][j] = k,這樣每次松弛,都會被記錄在 path 矩陣中path 矩陣完成后,如測試2中的截圖所示
-
getAllShortestPath函數(shù)- 構(gòu)造字典 ShortestPath_dict
- 兩兩遍歷圖中各點(diǎn),循環(huán)調(diào)用
getShortestPath函數(shù)
-
getShortestPath函數(shù)- 如果兩個頂點(diǎn)不可達(dá)或者這兩個頂點(diǎn)為同一個頂點(diǎn),那么直接說明不可達(dá),且返回值為空列表:
[] - 如果兩個頂點(diǎn)不需要松弛就可以得到最短路徑,那么在 path 矩陣中對應(yīng)點(diǎn)的值就為初始值 -1,這代表了這兩個頂點(diǎn)鄰接,在
getShortestPath函數(shù)中是需要判斷的 - 如果兩個頂點(diǎn)需要松弛才能得到最短路徑,那么在 path 矩陣中對應(yīng)點(diǎn)的值就不為 -1,那么需要遞歸調(diào)用
back_path函數(shù)
- 如果兩個頂點(diǎn)不可達(dá)或者這兩個頂點(diǎn)為同一個頂點(diǎn),那么直接說明不可達(dá),且返回值為空列表:
-
back_path函數(shù)- 之前說了,path 矩陣中記錄的是每次松弛操作時的中間點(diǎn),所以在函數(shù)中前后兩次調(diào)用本身
-
舉例說明
- 以測試2的截圖為例,嘗試頂點(diǎn)0到頂點(diǎn)3的最短路徑:
- 進(jìn)入
getShortestPath(graph,path,i,j),調(diào)用back_path(path,0,3,[0]) - 首先調(diào)用
back_path(path,0,3,[0]),發(fā)現(xiàn) path[0][3]=5,說明需要頂點(diǎn)5的中轉(zhuǎn) - 于是調(diào)用
back_path(path,0,5,[0]),發(fā)現(xiàn) path[0][5]=2,說明需要頂點(diǎn)2的中轉(zhuǎn) - 繼續(xù)調(diào)用
back_path(path,0,2,[0]),發(fā)現(xiàn)到頭了,頂點(diǎn)0和頂點(diǎn)2直接鄰接,于是不再遞歸下去了,添加頂點(diǎn)2到最短路徑 shortestPath,此時 [0,2] - 返回上層,調(diào)用
back_path(path,2,5,[0,2]),發(fā)現(xiàn)到頭了,頂點(diǎn)2和頂點(diǎn)5直接鄰接,于是不再遞歸下去了,添加頂點(diǎn)5到最短路徑 shortestPath,此時 [0,2,5] - 返回上層,調(diào)用
back_path(path,5,3,[0,2,5]),發(fā)現(xiàn)需要頂點(diǎn)4的中轉(zhuǎn) - 于是調(diào)用
back_path(path,5,4,[0,2,5]),發(fā)現(xiàn)到頭了,不再遞歸,添加頂點(diǎn)4到最短距離,此時 [0,2,5,4] - 返回上層,調(diào)用
back_path(path,4,3,[0,2,5,4]),發(fā)現(xiàn)到頭了,不再遞歸,添加頂點(diǎn)3到最短距離,此時 [0,2,5,4,3] 已經(jīng)是最短距離了 - 注意:因?yàn)橹灰{(diào)用
back_path就會添加頂點(diǎn)進(jìn)入最短路徑 shortestPath,為了避免重復(fù),加入了 if 判斷,直接過濾掉重復(fù)的頂點(diǎn),因?yàn)樽疃搪窂讲豢赡芾@圈子。
- 進(jìn)入
- 以測試2的截圖為例,嘗試頂點(diǎn)0到頂點(diǎn)3的最短路徑:
源碼地址:https://github.com/edisonleolhl/DataStructure-Algorithm/blob/master/Graph/ShortestPath/floyd.py



