1. 程式人生 > 其它 >資料結構與演算法——弗洛伊德(Floyd)演算法

資料結構與演算法——弗洛伊德(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) 作為 中間頂點,距離表和前驅關係更新為:

    1. 找出以 A 為中間頂點的路徑:

      • C-A-G:距離為 9
      • C-A-B:距離為 12
      • G-A-B:距離為 7

      因為圖是無向的,只需要計算一個方向的即可。

    2. 更新距離表,需要與上一次的距離表作為參照對比

      • 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

    3. 如何找出以 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 件事情:

  1. 複用了之前的無向圖和列印功能
  2. 初始化弗洛伊德演算法中用到的 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

對於上面的描述,下面用圖例解釋下

  1. A-G-F,設定 A 到達 F 的前驅是 G,A,F = 6,上圖中的下標 6 就是 G

    這個是正確的

  2. G-F-D,設定 G 到達 D 的前驅是 F,G,D = 5,上圖中的下標 5 就是 F

  3. 那麼 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