數據結構與算法——普利姆(Prim)算法


應用場景-修路問題

勝利鄉有 7 個村庄(A, B, C, D, E, F, G) ,現在需要修路把 7 個村庄連通,各個 村庄的距離用邊線表示(權) ,比如 A – B 距離 5 公里

問:如何修路保證各個村庄都能連通,並且總的修建公路 總里程最短?

思路:

  • 只滿足連通:將 10 條邊,連接即可,但是總的里程數不是最小.
  • 滿足連通,又保證總里程最短:就是盡可能的選擇少的路線,並且每條路線最小,保證總里程數最少

最小生成樹

修路問題本質就是就是 最小生成樹問題最小生成樹(Minimum Cost Spanning Tree),簡稱 MST。

給定一個 帶權的無向連通圖,如何選取一棵生成樹,使樹上所有邊上權的總和為最小,這叫最小生成樹 ,它有如下特點:

  • N 個頂點,一定有 N-1 條邊
  • 包含全部頂點
  • N-1 條邊都在圖中

比如下圖:

求最小生成樹的算法主要是 普里姆算法(prim克魯斯卡爾算法(kruskal

普利姆算法(prim)介紹

普利姆(Prim)算法求最小生成樹,也就是:在包含 n 個頂點的連通圖中,找出只有(n-1)條邊包含所有 n 個頂點的連通子圖,也就是所謂的 極小連通子圖

它的算法如下:

  • G=(V,E) 是連通網
  • T=(U,D) 是最小生成樹
  • V、U 是頂點集合
  • E、D 是邊的集合
  1. 若從頂點 u 開始構造最小生成樹,則從集合 V 中取出頂點 u 放入集合 U 中,標記頂點 v 的visited[u]=1(說明該點已經被連接)
  2. 若集合 U 中頂點 ui 與集合 V-U 中的頂點 vj 之間存在邊,則尋找這些邊中權值最小的邊,但不能構成回路,將頂點 vj 加入集合U中,將邊(ui,vj)加入集合 D 中,標記 visited[vj]=1
  3. 重復步驟 ②,直到 U 與 V 相等,即所有頂點都被標記為訪問過,此時 D 中有 n-1 條邊

上面說的可能有點抽象難懂,不過沒事,請繼續往下看,就懂了。

普利姆算法圖解

以這個為例子:

  1. 從 A 點開始處理

    與 A 直連的點有:

    • A,C [7]:后面中括號中的是邊的權值
    • A,B [5]
    • A,G [2]

    在這個所有的邊中,A,G [2] 的權值最小,那么結果是:A、G

  2. A、G 開始,找到他們的直連邊,但是不包含已經訪選擇過的邊。

    1. A,C [7]
    2. A,B [5]
    3. G,B [3]
    4. G,E [4]
    5. G,F [6]

    在以上邊中,權值最小的是:G,B [3],那么結果是:A、G、B

  3. A、G、B 開始,找到他們的直連邊,但是不包含已經訪選擇過的邊。

    1. A,C [7]
    2. A,B [5]
    3. G,E [4]
    4. G,F [6]
    5. B,D [9]

    在以上邊中,權值最小的是:G,E [4],那么結果是:A、G、B、E

    以此類推

  4. A、G、B、E → 權值最小的邊是 E,F [5]A、G、B、E、F

  5. A、G、B、E、F → 權值最小的邊是 F,D [4]A、G、B、E、F、D

  6. A、G、B、E、F、D → 權值最小的邊是 A,C [7]A、G、B、E、F、D、C

那么最終結果則為下圖:

總里程數量為:2+3+4+5+4+7=25

動圖:

代碼實現

成圖

/**
 * 普利姆算法
 */
public class PrimAlgorithm {
    /**
     * 圖:首先需要有一個帶權的連通無向圖
     */
    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();
    }
}

測試輸出

        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  

使用鄰接矩陣構建了一個無向圖,按照題目中的連通要求進行構建。

最小生成樹

    /**
     * 最小生成樹
     */
    class MinTree {
        /**
         * 普利姆算法
         *
         * @param mGraph 無向圖
         * @param v      從哪一個點開始生成
         */
        public void prim(MGraph mGraph, int v) {
            int minTotalWeight = 0; // 記錄已選擇的邊的總權值,僅僅只是為了測試打印驗證,不包含在算法中

            // 記錄已經選擇過的節點
            boolean[] selects = new boolean[mGraph.vertex];
            // 從哪個節點開始,則標識已經被訪問
            selects[v] = true;

            // 一共要生成 n-1 條邊
            for (int i = 1; i < mGraph.vertex; i++) {
                // 每次循環:選擇一條權值最小的邊出來

                // 記錄最小值
                int minWeight = 10000;
                //村庄索引
                int x = -1;//已經訪問過的村庄索引
                int y = -1;//剛建立連接的村庄索引

                // 每次查找權值最小的邊:根據思路,每次都是從已經選擇過的點,中去找與該點直連的點的權值
                // 並且該點還沒有被選擇過:如果兩個點都被選擇過,要么他們是雙向的,要么就是被其他的點選擇過了
                // 這里雙循環的含義:建議對照上面解析步驟分析理解
                for (int j = 0; j < mGraph.vertex; j++) { //j的含義是已經訪問過的村庄索引,對照for循環體中的判斷語句理解
                    for (int k = 0; k < mGraph.vertex; k++) {//k的含義是未被訪問過的村庄索引,同樣對照for循環體中的判斷語句理解
                        // 通過 j 來限定已經選擇過的點
                        // 通過 k 來遍歷匹配,沒有選擇過的點
                        // 假設第一輪是 A 點:j = 0
                        // 那么在這里將會執行 0,1  0,2, 0,3 也就是與 A 點直連,且沒有被選擇過的點,的最小權值
                        if (selects[j] && !selects[k]
                                && mGraph.weights[j][k] < minWeight
                        ) {
                            // 記錄最小權值,與這條邊的信息
                            minWeight = mGraph.weights[j][k];
                            x = j;
                            y = k;
                        }
                    }
                }
                
                // 當一次循環結束時:就找到了一條權值最小的邊
                System.out.printf("%s,%s [%s] \n", mGraph.datas[x], mGraph.datas[y], mGraph.weights[x][y]);
                minTotalWeight += mGraph.weights[x][y]; // 統計已選擇邊權值,單純為了打印
                
                //重置最小權值,必做步驟
                minWeight = 10000;
                // 記錄該點已經被選擇
                // 在查找最小權值邊的代碼中:y=k, k 表示沒有被選擇過的點,所以,找到之后,這里記錄 y 為這條邊的另一個點,標記以訪問
                selects[y] = true;
            }
            System.out.println("總權值:" + minTotalWeight);
        }
    }

   /**
     * 圖:首先需要有一個帶權的連通無向圖
     */
    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 primTest() {
        // 不連通的默認值:
        // 這里設置為較大的數,是為了后續的計算方便,計算權值的時候,不會選擇
        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();

        MinTree minTree = new MinTree();
        minTree.prim(mGraph, 0);

    }

測試輸出

        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  
A,G [2] 
G,B [3] 
G,E [4] 
E,F [5] 
F,D [4] 
A,C [7] 
總權值:25

測試結果也就是這個圖

動圖(從B點開始):

下面看看起始點不同的輸出信息,從 B 點開始:minTree.prim(mGraph, 1);

B,G [3] 
G,A [2] 
G,E [4] 
E,F [5] 
F,D [4] 
A,C [7] 
總權值:25

可以看到:順序不同,但是邊和總權值是相同的。


免責聲明!

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



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