算法:以不相交集類(DisjointSet)作爲輔助工具類,用Java實現克魯斯卡爾(Kruskal)算法求出圖的最小生成樹

之前我用克魯斯卡爾(Kruskal)算法求出了圖的最小生成樹(還記得是如何求出最小生成樹的嗎?就是把圖的所有邊拿出來,按權重從小到大排列,依次拿邊嘗試放到圖中。如果邊放進去不形成環,則放進去。如果邊放進去會形成環,則這條邊捨棄,然後繼續嘗試下一條邊,直到圖中所有點連通,共需要圖頂點數-1條邊),因爲當時還不知道不相交集類這麼個數據結構與算法,所以在判斷一條邊是接受還是捨棄的時候,用的鄰接矩陣的的廣度優先遍歷算法(BFS)來判斷兩個頂點是否是連通的(點我查看)。現在有了不相交集類這麼個利器,實現克魯斯卡爾(Kruskal)算法就更加方便了。

我們在往圖中一條一條添加邊的過程中,剛開始可以看做所有的頂點都是不等價的。然後加入一條邊,也就是將該邊的兩個頂點合併,然後這兩個頂點就等價了,也就是連通了。每次加入邊之前需要判斷該邊的兩個頂點是否等價,也就是是否連通。如果等價,則代表兩個頂點已經連通了,該邊需要捨棄,否則就有環了。如果不等價,則代表兩個頂點不連通,則接受該邊,然後再將該邊的兩個頂點求並。直到接受的邊等於頂點數-1,或者所有的邊都已經經過以上算法的計算,算法結束,所有組成圖的最小生成樹的邊已全部得到。

我們再梳理一下不相交集類和圖的對應關係:

  1. 圖的所有頂點,就對應不相交集類的所有元素
  2. 如果圖的兩個頂點連通,就對應不相交集類的兩個元素的等價
  3. 如果圖的兩個頂點不連通,也就對應不相交集類的兩個元素不等價
  4. 在求最小生成樹的過程中,兩個頂點判定爲不連通(不等價),然後加入這條邊,就對應不相交集類的兩個元素的求並操作
  5. 最後用克魯斯卡爾(Kruskal)算法求出了最小生成樹,圖的意義就是所有頂點均連通了,不相交集類的意義就是所有元素都等價了
  6. 所以,圖的頂點的連通關係,就一一對應不相交集類的等價關係。合併操作就是加邊,加邊就是對應合併。當然,如果已經生成最小生成樹,還繼續加邊,則對應合併操作(雖然此後的加邊對應的合併,兩個頂點的根頂點相同,不進行合併操作)依然有效。也就是說,無論加多少邊,不相交集類依然可以通過等價關係,判定兩個頂點是否連通。甚至可以通過之前對不相交集類的統計方法(點 這裏 查看博文的arrangeDisjointSet()方法),求出哪些頂點是連通的,最後得到整個圖的所有頂點的連通關係

我們還是拿那個示例圖來舉例子,我們要求下面這張圖的最小生成樹:
在這裏插入圖片描述
下面的這張圖,就是上面示例圖的最小生成樹:
在這裏插入圖片描述
根據示例圖的最小生成樹,我們可以得到,V1-V7這七個頂點已經連通,但是連通他們的邊的權重和是最小的。你找不到更小的連通圖,連通邊的權重和比上面的圖還要小了。連通他們的最小生成樹的組成邊爲:

  1. V1 - V4,權重爲1
  2. V6 - V7,權重爲1
  3. V1 - V2,權重爲2
  4. V3 - V4,權重爲2
  5. V4 - V7,權重爲4
  6. V5 - V7,權重爲6

從上面的答案,我們也可以看出,最小生成樹組成的邊的數量,就是頂點數-1,也驗證了之前的定理。如何用不相交集類作爲輔助工具類,用克魯斯卡爾(Kruskal)算法求出上面示例圖的最小生成樹呢?實現的代碼如下,算法的思想和精髓都在代碼和其間的詳細註釋中:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * @author LiYang
 * @ClassName KruskalByDisjointSet
 * @Description 用不相交集類(DisjointSet),實現克魯斯卡爾(Kruskal)算法
 * @date 2019/11/18 14:20
 */
public class KruskalByDisjointSet {

    /**
     * 圖的邊類
     * 剛開始需要將所有的邊按權重,從小到大排列,
     * 然後依次嘗試加入到圖中,直到生成最小生成樹
     */
    static class Edge implements Comparable<Edge> {
        
        //頂點1的下標(到時候對應String[] vertexName)
        private int vertex1;
        
        //頂點2的下標(到時候對應String[] vertexName)
        private int vertex2;
        
        //邊的權重
        private int weight;

        /**
         * 全參數的構造方法
         * @param vertex1 頂點1的下標
         * @param vertex2 頂點2的下標
         * @param weight 邊的權重
         */
        public Edge(int vertex1, int vertex2, int weight) {
            this.vertex1 = vertex1;
            this.vertex2 = vertex2;
            this.weight = weight;
        }
        
        public int getVertex1() {
            return vertex1;
        }

        public int getVertex2() {
            return vertex2;
        }

        public int getWeight() {
            return weight;
        }

        /**
         * 重寫compareTo方法,按邊的權重從小到大排序
         * @param o
         * @return
         */
        @Override
        public int compareTo(Edge o) {
            return this.weight - o.weight;
        }
    }

    /**
     * 不相交集工具類,按高度來合併
     * 實現代碼根之前的不相交集類一樣
     * 這裏作爲內部類,輔助實現Kruskal算法
     */
    static class DisjointSetUnionByHeight {
        //不相交集類(並查集)的頂點數組
        private int[] vertexes;

        /**
         * 不相交集類(並查集)的構造方法,入參頂點個數
         * @param vertexNum 頂點個數
         */
        public DisjointSetUnionByHeight(int vertexNum) {
            if (vertexNum <= 0) {
                throw new IllegalArgumentException("頂點個數要大於零");
            }

            //實例化不相交集類(並查集)的頂點數組
            this.vertexes = new int[vertexNum];

            //初始化頂點樹的高度都爲-1(如果是根,值就是負數,連通頂點組成的樹
            //的高度是多少,則根元素就是負幾)
            for (int i = 0; i < vertexes.length; i++) {
                this.vertexes[i] = -1;
            }
        }

        /**
         * 查詢不相交集類(並查集)的頂點個數
         * @return 頂點個數
         */
        public int size() {
            return vertexes.length;
        }

        /**
         * 查詢不相交集類(並查集)的某個頂點的根元素
         * 輸入的是下標查,如果兩個元素的根元素相同,
         * 則這兩個元素就是等價的。實際中還會有一個
         * 與vertexes等長的數組,裝的是頂點的名字,
         * vertexes只是相當於代號,記錄連通關係,
         * 二者通過下標,來映射真實頂點
         * @param vertexIndex 待查詢的頂點下標
         * @return 該頂點的根頂點
         */
        public int find(int vertexIndex) {
            //如果記錄小於0,那就是根頂點
            if (vertexes[vertexIndex] < 0) {
                //返回根頂點
                return vertexIndex;

            //如果記錄不小於0,那還不是根,
            //是等價森林中的上一個節點
            } else {
                //遞歸向上繼續尋找根
                return find(vertexes[vertexIndex]);
            }
        }

        /**
         * 將不相交集類(並查集)的兩個頂點進行連通操作
         * 注意,兩個頂點連通,代表這兩個頂點所在的子樹
         * 全部變成一個圖中的大的子樹。如果這
         * 兩個頂點本來就連通,則不進行連通操作,捨棄該邊。
         * 注意,這裏同樣是入參下標,下標映射真實頂點
         * 此實現類,根據樹的高度來決定誰合併到誰上面,
         * 矮的樹的根節點,會作爲大的樹的根節點的子節點
         * @param vertexIndex1 頂點下標1
         * @param vertexIndex2 頂點下標2
         */
        public void union(int vertexIndex1, int vertexIndex2) {
            int root1 = find(vertexIndex1);
            int root2 = find(vertexIndex2);

            //如果兩個頂點本就連通
            if (root1 == root2) {
                //不作處理
                return;
            }

            //比高度:如果root1比root2的樹要高
            if (vertexes[root1] < vertexes[root2]) {
                //將較矮的root2合併到較高的root1上
                vertexes[root2] = root1;

            //比高度:如果root2比root1的樹要高
            } else if (vertexes[root2] < vertexes[root1]) {
                //將較矮的root1合併到較高的root2上
                vertexes[root1] = root2;

            //比高度:如果root1和root2一樣高
            } else {
                //將root1合併到root2上
                vertexes[root1] = root2;

                //root2的高度增加1
                root2 --;
            }
        }
    }

    /**
     * 用不相交集類作爲輔助,用克魯斯卡爾(Kruskal)算法求出示例圖的最小生成樹
     * @param edgeList 圖的所有邊的List
     * @param vertexNum 圖的頂點的個數
     * @param vertexName 圖的頂點的
     * @return 組成示例圖的最小生成樹的所有邊
     */
    public static List<Edge> runKruskalByDisjointSet(List<Edge> edgeList, int vertexNum, String[] vertexName) {
        //最後組成示例圖的最小生成樹的邊的集合
        List<Edge> findKruskalEdges = new ArrayList<>();
        
        //將所有的邊,按照權重從小到大排序
        Collections.sort(edgeList);
        
        //創建不相交集類的實例,本例我們用按高度求並的不相交集類,並初始化每個頂點都不連通
        DisjointSetUnionByHeight disjointSet = new DisjointSetUnionByHeight(vertexNum);
        
        //按權重從小到大,遍歷每一條邊
        for (Edge edge : edgeList) {
            //找到當前邊的第一個頂點的根頂點
            int rootVertex1 = disjointSet.find(edge.getVertex1());
            
            //找到當前邊的第二個頂點的根頂點
            int rootVertex2 = disjointSet.find(edge.getVertex2());
            
            //如果當前邊的兩個頂點,不連通(不在等價集合裏就不連通)
            if (rootVertex1 != rootVertex2) {
                //接受當前邊,並作爲最小生成樹中的一條邊
                findKruskalEdges.add(edge);
                
                //合併當前邊的兩個頂點(也就是連通這兩個頂點)
                disjointSet.union(edge.getVertex1(), edge.getVertex2());
                
                //輸出算法的操作過程
                System.out.println(String.format("邊: (%s -- %s), 權重: %d : 接受",
                        vertexName[edge.getVertex1()], vertexName[edge.getVertex2()],edge.getWeight()));
            
            //如果當前邊的兩個頂點是連通的
            } else {
                //捨棄當前邊,也就是最小生成樹不該包含當前邊,並輸出算法的操作過程
                System.out.println(String.format("邊: (%s -- %s), 權重: %d : 捨棄",
                        vertexName[edge.getVertex1()], vertexName[edge.getVertex2()],edge.getWeight()));
            }
            
            //如果接收的邊的數量已經達到頂點數-1了
            if (findKruskalEdges.size() == vertexNum - 1) {
                //表示所有頂點均已連通,最小生成樹已求得
                break;
            }
        }
        
        //結束用不相交集類作爲輔助的克魯斯卡爾(Kruskal)算法
        //並返回最終組成最小生成樹的所有邊
        return findKruskalEdges;
    }

    /**
     * 驗證用不相交集類作爲輔助的克魯斯卡爾(Kruskal)算法
     * 求出示例圖的最終組成最小生成樹的所有邊
     * @param args
     */
    public static void main(String[] args) {
        //將示例圖中的所有邊,初始化並放入edgeList
        List<Edge> edgeList = new ArrayList<>();
        edgeList.add(new Edge(0, 3, 1));
        edgeList.add(new Edge(5, 6, 1));
        edgeList.add(new Edge(0, 1, 2));
        edgeList.add(new Edge(2, 3, 2));
        edgeList.add(new Edge(1, 3, 3));
        edgeList.add(new Edge(0, 2, 4));
        edgeList.add(new Edge(3, 6, 4));
        edgeList.add(new Edge(2, 5, 5));
        edgeList.add(new Edge(4, 6, 6));
        
        //示例圖的頂點數爲7
        int vertexNum = 7;
        
        //示例圖的頂點下標對應的頂點名稱
        String[] vertexName = {"V1", "V2", "V3", "V4", "V5", "V6", "V7"};

        //運行不相交集類作爲輔助的克魯斯卡爾(Kruskal)算法,
        //得到示例圖的最小生成樹的所有組成邊
        List<Edge> acceptedEdgeList = runKruskalByDisjointSet(edgeList, vertexNum, vertexName);

		//空行
        System.out.println();
        
        //查看組成示例圖的最小生成樹的所有邊
        for (Edge edge : acceptedEdgeList) {
            System.out.println(String.format("示例圖的最小生成樹的組成邊:(%s -- %s)",
                    vertexName[edge.getVertex1()], vertexName[edge.getVertex2()]));
        }
    }

}

其中,圖的邊的類,以及不相交集類輔助類,都是以內部類實現的,運行 KruskalByDisjointSet 類的main方法,初始化示例圖的邊的參數和其他參數,運行 runKruskalByDisjointSet() 的算法驅動方法,輸出結果如下(控制檯輸出,包括求示例圖的最小生成樹的算法運行過程中邊的接受和捨棄的判斷,以及最後求得的示例圖的最小生成樹的所有組成邊的信息展示):

: (V1 -- V4), 權重: 1 : 接受
邊: (V6 -- V7), 權重: 1 : 接受
邊: (V1 -- V2), 權重: 2 : 接受
邊: (V3 -- V4), 權重: 2 : 接受
邊: (V2 -- V4), 權重: 3 : 捨棄
邊: (V1 -- V3), 權重: 4 : 捨棄
邊: (V4 -- V7), 權重: 4 : 接受
邊: (V3 -- V6), 權重: 5 : 捨棄
邊: (V5 -- V7), 權重: 6 : 接受

示例圖的最小生成樹的組成邊:(V1 -- V4)
示例圖的最小生成樹的組成邊:(V6 -- V7)
示例圖的最小生成樹的組成邊:(V1 -- V2)
示例圖的最小生成樹的組成邊:(V3 -- V4)
示例圖的最小生成樹的組成邊:(V4 -- V7)
示例圖的最小生成樹的組成邊:(V5 -- V7)

根據控制檯的輸出結果,可以說明:上述代碼實現的算法已成功求得了示例圖的最小生成樹,而且用不相交集類作爲輔助工具類,也很好地幫助判斷了算法過程中邊接受或捨棄操作的選擇!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章