資料結構與演算法——弗洛伊德(Floyd)演算法
介紹
和 Dijkstra 演算法一樣,弗洛伊德(Floyd)演算法 也是一種用於尋找給定的加權圖中頂點間最短路徑的演算法。該演算法名稱以創始人之一、1978 年圖靈獎獲得者、斯坦福大學計算機科學系教授羅伯特·弗洛伊德命名
弗洛伊德演算法(Floyd)計算圖中 各個頂點之間 的最短路徑,比如:先從 A 出發到各個點的最短路徑,再從 B 出發,直到所有節點距離各個點的路徑都會計算出來。而迪傑斯特拉演算法用於計算圖中 某一個頂點到其他頂點的最短路徑。
弗洛伊德演算法 VS 迪傑斯特拉演算法:
- 迪傑斯特拉演算法通過選定的被訪問頂點,求出從出發訪問頂點到其他頂點的最短路徑;(關於Dijkstra 演算法可以看
- 弗洛伊德演算法中每一個頂點都是出發訪問點,所以需要將每一個頂點看做被訪問頂點,求出從每一個頂點到其他頂點的最短路徑。
核心思想
設:
- 頂點 vi 到頂點 vk 的最短路徑已知為Lik,
- 頂點 vk 到 vj 的最短路徑已知為 Lkj
- 頂點 vi 到 vj 的路徑為 Lij
則 vi 到 vj 的最短路徑為:min((Lik+Lkj),Lij)
,vk 的取值為圖中所有頂點,則可獲得 vi 到 vj 的最短路徑(則:假設三個點(不一定是具體的是 3 個點),一個直達,一個間接到達,算哪個路徑最短)
至於 vi 到 vk 的最短路徑 Lik 或者 vk 到 vj 的最短路徑 Lkj,是以同樣的方式獲得
圖解
以前面的公交站距離圖解
上圖中含義:
- 0:表示自己與自己。如 A,A
- N:表示不可直連
初始前驅頂點如上圖:每個節點到達其他節點的初始前驅都是它自己。
-
第一輪迴圈中,以 A (下標為:0) 作為 中間頂點,距離表和前驅關係更新為:
-
找出以 A 為中間頂點的路徑:
C-A-G
:距離為 9C-A-B
:距離為 12G-A-B
:距離為 7
因為圖是無向的,只需要計算一個方向的即可。
-
更新距離表,需要與上一次的距離表作為參照對比
C-A-G
:距離為 9,原始C,G
距離為 N,9 < N,則更新C,G = 9
C-A-B
:距離為 12,原始C,B
距離為 N,12 < N,則更新C,B=12
G-A-B
:距離為 7,原始G,B
距離為 3,則不更新
另外由於是無向圖,更新其中一個,那麼另外一個方向的也要同步更新。
同時:更新對應位置的前驅節點為 A
-
如何找出以 A 為中間頂點的所有路徑?
// 使用 3 個數組來實現,思路如下 中間頂點:[A,B,C,D,E,F,G] k=0 出發頂點:[A,B,C,D,E,F,G] i=0,1.... 終點頂點:[A,B,C,D,E,F,G] j=0,1,2... 以 k 為中間頂點時,使用一個雙層迴圈,來遍歷出所有的情況 並在這個尋找路徑的迴圈中,找出最短路徑,去更新上述所演示的:距離表、前驅表
當把 A 作為中間頂點路徑尋找完成之後,表中的資料則為 A 到所有頂點的最短距離。
當所有頂點都更新之後,最後就是每個頂點到其他頂點的最短距離(注:所有頂點沒有更新完成之前,最終結果不一定是最短的,後續可能還會更新)
看上去很簡單,就是一個三層 for 迴圈,但是它的時間複雜度是 n3,比如這裡有 7 個節點,那麼迴圈的次數是
7x7x7=343
-
弗洛伊德演算法最佳應用-最短路徑
勝利鄉有 7 個村莊 (A, B, C, D, E, F, G)
,各個村莊的距離用邊線表示(權),比如 A-B 距離為 5 公里
問:如何計算出 各村莊到其他各村莊的最短距離?
下面直接用程式碼實現,前面圖解和思路都說了。
準備工作
主要做了 3 件事情:
- 複用了之前的無向圖和列印功能
- 初始化弗洛伊德演算法中用到的 3 個數組和初始化
- 列印狀態和結果
/**
* 佛洛依德演算法-最短路徑
*/
public class FloydAlgorithm {
/**
* 圖:首先需要有一個帶權的連通無向圖
*/
class MGraph {
int vertex; // 頂點個數
int[][] weights; // 鄰接矩陣
char[] datas; // 村莊資料
/**
* @param vertex 村莊數量, 會按照數量,按順序生成村莊,如 A、B、C...
* @param weights 需要你自己定義好那些點是連通的,那些不是連通的
*/
public MGraph(int vertex, int[][] weights) {
this.vertex = vertex;
this.weights = weights;
this.datas = new char[vertex];
for (int i = 0; i < vertex; i++) {
// 大寫字母 A 從 65 開始
datas[i] = (char) (65 + i);
}
}
public void show() {
System.out.printf("%-8s", " ");
for (char vertex : datas) {
// 控制字串輸出長度:少於 8 位的,右側用空格補位
System.out.printf("%-8s", vertex + " ");
}
System.out.println();
for (int i = 0; i < weights.length; i++) {
System.out.printf("%-8s", datas[i] + " ");
for (int j = 0; j < weights.length; j++) {
System.out.printf("%-8s", weights[i][j] + " ");
}
System.out.println();
}
}
}
@Test
public void mGraphTest() {
// 不連通的預設值:
// 這裡設定為較大的數,是為了後續的計算方便,計算權值的時候,不會選擇
int defaultNo = 100000;
int[][] weights = new int[][]{
{defaultNo, 5, 7, defaultNo, defaultNo, defaultNo, 2}, // A
{5, defaultNo, defaultNo, 9, defaultNo, defaultNo, 3},// B
{7, defaultNo, defaultNo, defaultNo, 8, defaultNo, defaultNo},// C
{defaultNo, 9, defaultNo, defaultNo, defaultNo, 4, defaultNo},// D
{defaultNo, defaultNo, 8, defaultNo, defaultNo, 5, 4},// E
{defaultNo, defaultNo, defaultNo, 4, 5, defaultNo, 6},// F
{2, 3, defaultNo, defaultNo, 4, 6, defaultNo}// G
};
MGraph mGraph = new MGraph(7, weights);
mGraph.show();
}
@Test
public void floydTest() {
int defaultNo = 100000;
int[][] weights = new int[][]{
{defaultNo, 5, 7, defaultNo, defaultNo, defaultNo, 2}, // A
{5, defaultNo, defaultNo, 9, defaultNo, defaultNo, 3},// B
{7, defaultNo, defaultNo, defaultNo, 8, defaultNo, defaultNo},// C
{defaultNo, 9, defaultNo, defaultNo, defaultNo, 4, defaultNo},// D
{defaultNo, defaultNo, 8, defaultNo, defaultNo, 5, 4},// E
{defaultNo, defaultNo, defaultNo, 4, 5, defaultNo, 6},// F
{2, 3, defaultNo, defaultNo, 4, 6, defaultNo}// G
};
MGraph mGraph = new MGraph(7, weights);
mGraph.show();
floyd(mGraph);
showFloydDis();
showFloydPre();
showFormat();
}
//弗洛伊德演算法中用到的 3 個數組
private char[] vertexs; // 存放頂點
private int[][] dis; // 從各個頂點出發到其他頂點的距離
private int[][] pre; // 到達目標頂點的前驅頂點
//這裡的程式碼還沒有寫完,缺少演算法的核心程式碼
public void floyd(MGraph mGraph) {
vertexs = mGraph.datas;
dis = mGraph.weights;
pre = new int[mGraph.vertex][mGraph.vertex];
// 初始化 pre
for (int i = 0; i < pre.length; i++) {
Arrays.fill(pre[i], i);
}
}
/**
* 顯示 dis 和 pre,這個資料也是最後的結果資料
*/
public void showFloydDis() {
System.out.println("dis 結果");
show(dis);
}
public void showFloydPre() {
System.out.println("pre 結果");
show(pre);
}
public void show(int[][] weights) {
System.out.printf("%-8s", " ");
for (char vertex : vertexs) {
// 控制字串輸出長度:少於 8 位的,右側用空格補位
System.out.printf("%-8s", vertex + " ");
}
System.out.println();
for (int i = 0; i < weights.length; i++) {
System.out.printf("%-8s", vertexs[i] + " ");
for (int j = 0; j < weights.length; j++) {
System.out.printf("%-8s", weights[i][j] + " ");
}
System.out.println();
}
}
/**
* 直接打印出我們的結果
*/
public void showFormat() {
System.out.println("最終結果格式化顯示:");
for (int i = 0; i < dis.length; i++) {
// 先將 pre 陣列輸出一行
System.out.println(vertexs[i] + " 到其他頂點的最短距離");
// 輸出 dis 陣列的一行資料
// 每一行資料是,一個頂點,到達其他頂點的最短路徑
for (int k = 0; k < dis.length; k++) {
System.out.printf("%-16s", vertexs[i] + " → " + vertexs[k] + " = " + dis[i][k] + "");
}
System.out.println();
System.out.println();
}
}
}
測試輸出
A B C D E F G
A 100000 5 7 100000 100000 100000 2
B 5 100000 100000 9 100000 100000 3
C 7 100000 100000 100000 8 100000 100000
D 100000 9 100000 100000 100000 4 100000
E 100000 100000 8 100000 100000 5 4
F 100000 100000 100000 4 5 100000 6
G 2 3 100000 100000 4 6 100000
dis 結果
A B C D E F G
A 100000 5 7 100000 100000 100000 2
B 5 100000 100000 9 100000 100000 3
C 7 100000 100000 100000 8 100000 100000
D 100000 9 100000 100000 100000 4 100000
E 100000 100000 8 100000 100000 5 4
F 100000 100000 100000 4 5 100000 6
G 2 3 100000 100000 4 6 100000
pre 結果
A B C D E F G
A 0 0 0 0 0 0 0
B 1 1 1 1 1 1 1
C 2 2 2 2 2 2 2
D 3 3 3 3 3 3 3
E 4 4 4 4 4 4 4
F 5 5 5 5 5 5 5
G 6 6 6 6 6 6 6
最終結果格式化顯示:
A 到其他頂點的最短距離
A → A = 100000 A → B = 5 A → C = 7 A → D = 100000 A → E = 100000 A → F = 100000 A → G = 2
B 到其他頂點的最短距離
B → A = 5 B → B = 100000 B → C = 100000 B → D = 9 B → E = 100000 B → F = 100000 B → G = 3
C 到其他頂點的最短距離
C → A = 7 C → B = 100000 C → C = 100000 C → D = 100000 C → E = 8 C → F = 100000 C → G = 100000
D 到其他頂點的最短距離
D → A = 100000 D → B = 9 D → C = 100000 D → D = 100000 D → E = 100000 D → F = 4 D → G = 100000
E 到其他頂點的最短距離
E → A = 100000 E → B = 100000 E → C = 8 E → D = 100000 E → E = 100000 E → F = 5 E → G = 4
F 到其他頂點的最短距離
F → A = 100000 F → B = 100000 F → C = 100000 F → D = 4 F → E = 5 F → F = 100000 F → G = 6
G 到其他頂點的最短距離
G → A = 2 G → B = 3 G → C = 100000 G → D = 100000 G → E = 4 G → F = 6 G → G = 100000
可以看到如上的輸出,能方便我們檢視狀態圖。
弗洛伊德演算法核心程式碼
就是三層迴圈處理
public void floyd(MGraph mGraph) {
vertexs = mGraph.datas;
dis = mGraph.weights;
pre = new int[mGraph.vertex][mGraph.vertex];
// 初始化 pre
for (int i = 0; i < pre.length; i++) {
Arrays.fill(pre[i], i);
}
// 從中間頂點的遍歷
for (int i = 0; i < vertexs.length; i++) {
// 出發頂點
for (int j = 0; j < vertexs.length; j++) {
// 終點
for (int k = 0; k < vertexs.length; k++) {
// 中間節點連線: 從 j 到 i 到 k 的距離
int lji = dis[j][i];
int lik = dis[i][k];
int leng = lji + lik;
// 直連
int ljk = dis[j][k];
// 如果間接距離比直連短,則更新
if (leng < ljk) {
dis[j][k] = leng;
/*
最難理解的是這裡:
i 是已知的中間節點,前驅的時候直接設定為 i (pre[j][k] = i;) ,結果是不對的。
比如:A-G-F-D , 中間節點是是 兩個節點,那麼 A 到 D 的前驅節點是 F,而不是 G
F 的前驅節點是 G
如果直接賦值 i,前驅節點就會計算錯誤。
理解步驟為:
1. A-G-F:距離 8
A-F : 不能直連
那麼設定:A,F 的前驅節點是 G; 對應這裡的程式碼是 j,i
2. G-F-D: 距離是 10
G-D:不能直連
那麼設定:G,D 的前驅節點是 F; 對應這裡的程式碼是 i,k
3. 那麼最終 A,D 的前驅節點是是什麼呢?
其實就應該是 G,D 指向的值; 對應這裡的程式碼是 i,k
*/
pre[j][k] = pre[i][k]; // 前驅節點更新為中間節點
}
}
}
}
}
測試輸出結果
A B C D E F G
A 100000 5 7 100000 100000 100000 2
B 5 100000 100000 9 100000 100000 3
C 7 100000 100000 100000 8 100000 100000
D 100000 9 100000 100000 100000 4 100000
E 100000 100000 8 100000 100000 5 4
F 100000 100000 100000 4 5 100000 6
G 2 3 100000 100000 4 6 100000
dis 結果
A B C D E F G
A 4 5 7 12 6 8 2
B 5 6 12 9 7 9 3
C 7 12 14 17 8 13 9
D 12 9 17 8 9 4 10
E 6 7 8 9 8 5 4
F 8 9 13 4 5 8 6
G 2 3 9 10 4 6 4
pre 結果
A B C D E F G
A 6 0 0 6 6 6 0
B 1 6 0 1 6 6 1
C 2 0 0 5 2 4 0
D 6 3 5 5 5 3 5
E 6 6 4 5 6 4 4
F 6 6 4 5 5 3 5
G 6 6 0 5 6 6 0
最終結果格式化顯示:
A 到其他頂點的最短距離
A → A = 4 A → B = 5 A → C = 7 A → D = 12 A → E = 6 A → F = 8 A → G = 2
B 到其他頂點的最短距離
B → A = 5 B → B = 6 B → C = 12 B → D = 9 B → E = 7 B → F = 9 B → G = 3
C 到其他頂點的最短距離
C → A = 7 C → B = 12 C → C = 14 C → D = 17 C → E = 8 C → F = 13 C → G = 9
D 到其他頂點的最短距離
D → A = 12 D → B = 9 D → C = 17 D → D = 8 D → E = 9 D → F = 4 D → G = 10
E 到其他頂點的最短距離
E → A = 6 E → B = 7 E → C = 8 E → D = 9 E → E = 8 E → F = 5 E → G = 4
F 到其他頂點的最短距離
F → A = 8 F → B = 9 F → C = 13 F → D = 4 F → E = 5 F → F = 8 F → G = 6
G 到其他頂點的最短距離
G → A = 2 G → B = 3 G → C = 9 G → D = 10 G → E = 4 G → F = 6 G → G = 4
關於前驅節點的計算
核心程式碼中有下面這樣一段註釋
i 是已知的中間節點,前驅的時候直接設定為 i (pre[j][k] = i;) ,結果是不對的。
比如:A-G-F-D , 中間節點是是 兩個節點,那麼 A 到 D 的前驅節點是 F,而不是 G
如果直接賦值 i,前驅節點就會計算錯誤。
理解步驟為:
1. A-G-F:距離 8
A-F : 不能直連
那麼設定:A,F 的前驅節點是 G; 對應這裡的程式碼是 j,i
2. G-F-D: 距離是 10
G-D:不能直連
那麼設定:G,D 的前驅節點是 F; 對應這裡的程式碼是 i,k
3. 那麼最終 A,D 的前驅節點是是什麼呢?
其實就應該是 G,D 指向的值; 對應這裡的程式碼是 i,k
對於上面的描述,下面用圖例解釋下
-
A-G-F,設定 A 到達 F 的前驅是 G,
A,F = 6
,上圖中的下標 6 就是 G這個是正確的
-
G-F-D,設定 G 到達 D 的前驅是 F,
G,D = 5
,上圖中的下標 5 就是 F -
那麼 A-G-F-D,設定 A 到達 D 的前驅是 ?
這裡需要這樣來看
A - G-F-D A - X 把 A-G-F-D 看成 A-X 而 X=G-F-D G-F-D, 的前驅節點是 F 則 A-X 的前驅節點是 F 則 A-D 的前驅節點是 F