數據結構與算法——弗洛伊德(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
    


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM