最小生成樹--Prim算法,基於優先隊列的Prim算法,Kruskal算法,Boruvka算法,“等價類”UnionFind


 最小支撐樹樹--Prim算法,基於優先隊列的Prim算法,Kruskal算法,Boruvka算法,“等價類”UnionFind

 

最小支撐樹樹

前幾節中介紹的算法都是針對無權圖的,本節將介紹帶權圖的最小支撐樹(minimum spanning tree)算法。給定一個無向圖G,並且它的每條邊均權值,則MST是一個包括G的所有頂點及邊的子集的圖,這個子集保證圖是連通的,並且子集中所有邊的權值之和為所有子集中最小的。

本節中介紹三種算法求解圖的最小生成樹:Prim算法、Kruskal算法和Boruvka算法。其中Prim算法將介紹兩種實現方法,一種是普通的貪心算法;而第二種算法是借助最大堆的貪心算法,其性能更高。Prim算法的思路是從任意一個頂點開始,逐步向已形成的MST子樹中增加權值最小的邊從而最終形成MST。Kruskal算法和Boruvka算法類似,都是向MST子樹的一個分布森林中增加邊來構建MST。

這三種算法都是貪心算法,有關貪心算法的討論請參閱相關書籍。貪心算法的思想是選擇當時最佳的路徑。一般而言,貪心策略不一定能保證找到全局最優解,但是對最小支撐樹問題來說,貪心策略能獲得具有最小權值的生成樹。

本節將使用前面章節中介紹的廣義樹來返回圖的最小生成樹。實現上述三個算法的類名稱為MinimumSpanningTree。

1 Prim算法

Prim算法可以說是所有MST算法中最簡單的,比較適用於稠密圖。以圖中任意一個頂點S開始,選擇與之相關連的邊中權值最小的邊加入到MST中,假設這條邊的終點為T,則MST初始化為(S, T),稱之為“當前MST”。接下來在剩余的邊中選擇與當前MST中s所有頂點相關連的邊中權值最小的邊,並添加到當前MST中。這一過程一直迭代到圖中所有頂點都添加到MST中為止。

在迭代時,假設當前MST中頂點形成集合Vs,則對Vs中的每一個vi,遍歷與其相鄰的所有邊,並找到權值最小的邊。這一過程實現如下:

復制代碼
/* Prim算法,求解圖G的最小生成樹
 * @param 輸入s:最小生成樹的樹根
 * @return 函數返回時V,D,mst都重新賦值
 */
public void Prim(int s){
    if(s < 0) 
        return;
    int nv = G.get_nv();
    // 初始化
    for(int i = 0; i < nv; i++){
        D[i] = Integer.MAX_VALUE;
        V[i] = -2;
        // 0 -- 沒添加到樹中
        G.setMark(i, 0);
    }
    
    // 對起點s,父節點為-1,距離為0
    V[s] = -1;
    D[s] = 0;
    G.setMark(s, 1);
    // 將起點添加到廣義樹中
    mst.addChild(0, s, new ElemItem<Double>(D[s]));

    /* 在其余頂點中找到與當前MST最近的頂點v,並將頂點的父節點和
     * 頂點v添加到MST中。其中圖的權值存放在節點v中。
     * 循環迭代,直至所有頂點都遍歷一遍 */
    while(true){
        /*獲取與當前樹距離最近的邊,其終點為最近的頂點
         * 終點為最近頂點的父節點 */
        Edge E = Utilities.minNextEdge(G, V);
        //如果邊為空,函數返回
        if(E == null) return;
        System.out.println(E.get_v1() + " -- "
                + E.get_v2() + " -- "
                + G.getEdgeWt(E));
        // E的起點賦值給V[E的終點]
        V[E.get_v2()] = E.get_v1();
        // E的權值賦值給D[E的終點]
        D[E.get_v2()] = G.getEdgeWt(E);
        // E的終點被訪問過了
        G.setMark(E.get_v2(), 1);
        // 在最小生成樹中添加邊E對應的信息
        mst.addChild(E.get_v1(), E.get_v2(),
                new ElemItem<Double>(D[E.get_v2()]));
    }
    
}
復制代碼

 

函數中對MinimumSpanningTree的數組V和D的值進行更新,V[i]的值表示最小支撐樹中中頂點i的父頂點,D[i]表示頂點i與其父頂點V[i]之間邊的權值。函數最終向當前對象的mst(最小支撐樹)中添加頂點,最終形成以起始頂點S為根節點的廣義樹樹結果(左子節點右兄弟節點樹)。

 

 

如圖所示,以頂點0為起點求解圖中最小支撐樹。按照程序流程,構造MST的詳細過程如圖(1~8)所示。

 

 

 

以圖做為測試並檢驗函數。起始頂點0,測試程序如下:

 

復制代碼
public static void main(String args[]){
    GraphLnk GL = 
        Utilities.BulidGraphLnkFromFile("Graph\\graph7.txt");
    MinimumSpanningTree MSTex = new MinimumSpanningTree(GL);
    MSTex.Prim(0);
    MSTex.mst.ford_print_tree();
    System.out.println("各頂點的父節點:");
    for(int i = 0; i < MSTex.V.length; i++){
        System.out.print(MSTex.V[i] + ", ");
    }
    System.out.println("\n各頂點距其父節點距離:");
    for(int i = 0; i < MSTex.D.length; i++){
        System.out.print((int)(MSTex.D[i]) + ", ");
    }
}
復制代碼

 

復制代碼
    程序運行結果如下:
0 -- 1 -- 4
0 -- 7 -- 8
7 -- 6 -- 1
6 -- 5 -- 2
5 -- 2 -- 4
2 -- 8 -- 2
2 -- 3 -- 7
3 -- 4 -- 9
9 節點,前序遍歷打印:
|—— 0.0(0)
    |—— 4.0(1)
    |—— 8.0(7)
        |—— 1.0(6)
            |—— 2.0(5)
                |—— 4.0(2)
                    |—— 2.0(8)
                    |—— 7.0(3)
                        |—— 9.0(4)
各頂點的父節點:
-1, 0, 5, 2, 3, 6, 7, 0, 2, 
各頂點距其父節點距離:
0, 4, 4, 7, 9, 2, 1, 8, 2,
復制代碼

 

結果第一部分為各頂點添加到最小生成樹mst中的順序;第二部分為mst的前序遍歷打印結果;最后一部分為各頂點在mst中的父節點以及與父節點形成的邊在圖中的權值。

2 基於優先隊列的Prim算法

Prim算法的流程比較容易理解,算法可以在線性時間內找出圖的MST。但是從實現過程可以發現,在遍歷與頂點相鄰的所有邊尋找與當前MST最近的頂點時,需要進行循環判斷,選擇距離最近的邊之后其余邊的信息將被舍棄;在下次循環判斷時這些舍棄的邊又被再次參與“競選”最近的邊,這無疑帶來了重復判斷。

可以引入一個最小堆,堆中存放遍歷過的邊。在遍歷與頂點相鄰的所有邊時,首先將連邊添加到堆中,然后直接將堆頂的邊取出並添加到MST中。這樣就可以避免重復判斷。算法的實現如下:

 

復制代碼
/*
     * 基於有線隊列的Prim算法;
     * 需要利用最小堆結構,但是我們之前只設計了最大堆,
     * 所以將堆節點中用於表示大小關系的元素值乘以-1,
     * 作用等效於最小堆;堆節點元素為 EdgeElem。
     * @param 起始頂點s
     */
    public void PrimPQ(int s){
        if(s < 0) return;
        // 圖頂點和圖邊個數
        int nv = G.get_nv();
        int ne = G.get_ne();
        // 堆最大為邊的條數
        MaxHeap H = new MaxHeap(ne);
        
        // 初始化
        for(int i = 0; i < nv; i++){
            D[i] = Integer.MAX_VALUE;
            V[i] = -2;
            // 0 -- 沒添加到樹中
            G.setMark(i, 0);
        }

        // 對起點s,父節點為-1,距離為0
        V[s] = -1;
        D[s] = 0;
        G.setMark(s, 1);
        // 將起點添加到廣義樹中
        mst.addChild(0, s, new ElemItem<Double>(D[s]));
        // 初始化堆,將與起點相連的邊都添加到堆中
        for(Edge E = G.firstEdge(s); G.isEdge(E); E = G.nextEdge(E)){
            D[E.get_v2()] = G.getEdgeWt(E);
            V[E.get_v2()] = s;
            H.insert(new ElemItem<EdgeElem>
                    (new EdgeElem(E, -1 * G.getEdgeWt(E))));
        }
        H.printHeap();
        
        // 將堆頂元素刪去並返回
        int v = -1; 
        EdgeElem PE = null;
        while(true){
            v = -1;
            // 如果堆不為空
            while(H.topVal() != null){
                // 刪除並返回堆頂元素
                PE = (EdgeElem)(H.removeMax().elem);
                H.printHeap();
                v = PE.get_v2();
                // 如果堆頂元素對應的頂點沒有被訪問,則退出循環
                if(G.getMark(v) == 0) 
                    break;
                // 否則表示沒有找到下一個可添加到MST的頂點
                else v = -1;
            }
            
            // 如果沒有可繼續添加的頂點了,函數返回
            if(v == -1) 
                return;
            
            // 將得到的堆頂元素對應頂點重置為訪問過
            G.setMark(v, 1);
            
            // 在最小生成樹中添加邊E對應的信息
            mst.addChild(PE.get_v1(), PE.get_v2(),
                    new ElemItem<Double>(D[PE.get_v2()]));
            
            // 繼續將與v相連的、未經訪問的邊添加到堆中
            for(Edge w = G.firstEdge(v); G.isEdge(w); w = G.nextEdge(w)){
                // 頂點尚未被訪問過,並且不在堆中
                if(G.getMark(w.get_v2()) == 0 
                        && D[G.edge_v2(w)] > G.getEdgeWt(w)){
                    D[G.edge_v2(w)] = G.getEdgeWt(w);
                    V[G.edge_v2(w)] = v;
                    H.insert(new ElemItem<EdgeElem>
                            (new EdgeElem(w, -1 * G.getEdgeWt(w))));
                }
            }
            H.printHeap();
        }
    }
復制代碼

 

由於在前面章節中我們只討論了最大堆,這里沒有為PrimPQ算法特別設計最小堆,而是在函數中對最大堆來稍作改變來實現最小堆的功能:在向堆中添加邊信息時,先將邊的權值取相反數。

圖中邊的信息包括邊的源點、終點和權值,由於基於連接表的圖數據結構中沒有這樣的邊數據結構,這里重新設計了邊數據結構作為堆中元素項。其設計如下:

 

 

復制代碼
/*
 * 繼承Comparable接口的EdgeElem類,其中包含一條邊和這條邊的權值
 */
public class EdgeElem implements Comparable{
    Edge e;
    int wt;
    // 構造函數
    public EdgeElem(Edge _e, int _wt){
        e = _e;
        wt = _wt;
    }
    // 獲取邊的起點
    public int get_v1(){
        return e.get_v1();
    }
    // 獲取邊的終點
    public int get_v2(){
        return e.get_v2();
    }
    // 獲取邊的權值
    public int get_wt(){
        return wt;
    }
    // 比較函數,比較對象為PrinElem的邊的權值
    public int compareTo(Object o) {
        EdgeElem other = (EdgeElem)o;
        if(wt > other.wt) return 1;
        else if(wt == other.wt) return 0;
        else return -1;
    }
    
    // 便於顯示,返回邊信息
    public String toString(){
        return "(" + e.get_v1() + ", "
                + e.get_v2() + "), " + wt;
    }
    
}
復制代碼

 

這里不對算法的詳細過程進行分析,通過示示例程序並跟蹤堆中元素項的變化來進行解釋,讀者可以自行分析算法流程。示例程序使用的圖為例。示例程序如下:

 

復制代碼
public class PrimPQExample {
    public static void main(String args[]){
        GraphLnk GL = 
            Utilities.BulidGraphLnkFromFile("Graph\\graph7.txt");
        MinimumSpanningTree MSTex = new MinimumSpanningTree(GL);
        MSTex.PrimPQ(0);
        MSTex.mst.ford_print_tree();
        System.out.println("各頂點的父節點:");
        for(int i = 0; i < MSTex.V.length; i++){
            System.out.print(MSTex.V[i] + ", ");
        }
        System.out.println("\n各頂點距其父節點距離:");
        for(int i = 0; i < MSTex.D.length; i++){
            System.out.print((int)(MSTex.D[i]) + ", ");
        }
    }
}
復制代碼

 

復制代碼
    堆中元素項變化(添加和刪除元素項時顯示)以及最終MST樹結果如下:
    
堆中元素旋轉90度分層打印:
(0, 1), -4
    (0, 7), -8
堆中元素旋轉90度分層打印:
(0, 7), -8
堆中元素旋轉90度分層打印:
(0, 7), -8
    (1, 2), -8
堆中元素旋轉90度分層打印:
(1, 2), -8
堆中元素旋轉90度分層打印:
    (7, 8), -7
(7, 6), -1
    (1, 2), -8
堆中元素旋轉90度分層打印:
(7, 8), -7
    (1, 2), -8
堆中元素旋轉90度分層打印:
    (7, 8), -7
(6, 5), -2
    (6, 8), -6
        (1, 2), -8
堆中元素旋轉90度分層打印:
    (7, 8), -7
(6, 8), -6
    (1, 2), -8
堆中元素旋轉90度分層打印:
    (7, 8), -7
        (5, 4), -10
(5, 2), -4
        (5, 3), -14
    (6, 8), -6
        (1, 2), -8
堆中元素旋轉90度分層打印:
    (7, 8), -7
(6, 8), -6
        (5, 3), -14
    (1, 2), -8
        (5, 4), -10
堆中元素旋轉90度分層打印:
        (7, 8), -7
    (6, 8), -6
        (2, 3), -7
(2, 8), -2
        (5, 3), -14
    (1, 2), -8
        (5, 4), -10
堆中元素旋轉90度分層打印:
    (7, 8), -7
        (2, 3), -7
(6, 8), -6
        (5, 3), -14
    (1, 2), -8
        (5, 4), -10
堆中元素旋轉90度分層打印:
    (7, 8), -7
        (2, 3), -7
(6, 8), -6
        (5, 3), -14
    (1, 2), -8
        (5, 4), -10
堆中元素旋轉90度分層打印:
    (7, 8), -7
(2, 3), -7
        (5, 3), -14
    (1, 2), -8
        (5, 4), -10
堆中元素旋轉90度分層打印:
    (5, 3), -14
(7, 8), -7
    (1, 2), -8
        (5, 4), -10
堆中元素旋轉90度分層打印:
    (5, 3), -14
(7, 8), -7
        (3, 4), -9
    (1, 2), -8
        (5, 4), -10
堆中元素旋轉90度分層打印:
    (5, 3), -14
(1, 2), -8
    (3, 4), -9
        (5, 4), -10
堆中元素旋轉90度分層打印:
    (5, 3), -14
(3, 4), -9
    (5, 4), -10
堆中元素旋轉90度分層打印:
(5, 4), -10
    (5, 3), -14
堆中元素旋轉90度分層打印:
(5, 4), -10
    (5, 3), -14
堆中元素旋轉90度分層打印:
(5, 3), -14
堆中元素旋轉90度分層打印:
9 節點,前序遍歷打印:
|—— 0.0(0)
    |—— 4.0(1)
    |—— 8.0(7)
        |—— 1.0(6)
            |—— 2.0(5)
                |—— 4.0(2)
                    |—— 2.0(8)
                    |—— 7.0(3)
                        |—— 9.0(4)
各頂點的父節點:
-1, 0, 5, 2, 3, 6, 7, 0, 2, 
各頂點距其父節點距離:
0, 4, 4, 7, 9, 2, 1, 8, 2,
復制代碼

 

程序打印結果的第一部分為堆中邊的動態變化情況;第二部分為生成的MST,其中根節點為算法的起始頂點;最后一部分表示的是各頂點的在MST中的父節點以及與父節點形成的邊的權值。

3 Kruskal算法

Prim算法和基於優先隊列的Prim算法的思路都是通過一次找出一條邊添加到MST中,其中每一步都是要找到一條新的邊並且關聯到不斷增長的當前MST。Kruskal算法也是一次找到一條邊來不斷構建MST,但是與Prim算法不同的是,它要找到 連接兩棵樹的一條邊,這兩棵樹處於一個MST子樹的分離的森林中,其中MST子樹將不斷增長。

算法由一個包括N棵(單個頂點組成的)樹的森林開始。然后持續完成合並兩棵樹的操作(使用最短的邊連接它們),直至只剩下一棵樹,這棵樹就是最終的MST樹。這一過程等效於:首先將每個頂點各自划分至一個“等價類”,共N個等價類,每次選擇最短的邊將等價類進行合並,直至只剩下一個等價類為止。

為保證連接等價類邊的權值最短,算法首先對圖中所有邊按照權值進行排序。按權值由小到大一次選擇邊,如果邊的兩頂點分別屬於不同的等價類,則將這條邊添加到MST並將這對頂點所屬的等價類合並。本節最后一部分將介紹的UnionFind類將是一個理想的數據結構,可以實現等價類的合並(Union)並判斷兩個頂點是否屬於同一個等價類(Find)。

基於以上的描述,Kruskal算法實現如下:

 

 

復制代碼
    /*
     * Krustral算法獲取最小支撐樹;算法借助UnionFind類
     */
    public void Krustral(){
        int nv = G.get_nv();
        int ne = G.get_ne();
        // 獲取圖上的每一條邊,並將邊按照權值由大到小排序
        ITEM[] E = Utilities.GetEdgeSort(G);
        // 集合形式的等價類
        UnionFind UF = new UnionFind(nv);
        // 待返回的EdgeElem數組
        R = new EdgeElem[nv]; 
        int r = 0;
        for(int i = 0, k = 1; i < ne && k < nv; i++){
            // 獲取一條邊
            EdgeElem pe = (EdgeElem)(E[i].getElem());
            int v1 = pe.get_v1();
            int v2 = pe.get_v2();
            // 如果這條邊的兩個頂點不在同一個等價類中
            if(UF.find(v1) != UF.find(v2)){
                // 則將這兩個頂點合並,
                UF.union(v1, v2);
                System.out.println(UF.toString());
                // 並將這條邊添加到EdgeElem數組中
                R[r++] = pe;
            }
        }
    }
復制代碼

 

算法中調用函數GetEdgeSort獲取圖中所有邊,並按照權值有小到大進行排序。GetEdgeSort函數首先將圖中所有邊取出,並以EdgeElem類的對象形式放置到ITEM類型的數組E中;然后調用第章中的任意一種排序算法對E中元素進行排序,其中EdgeElem類繼承的copmareTo函數比較的是兩個邊對象的權值。這里選擇快速排序算法,經過快速排序后E數組中的邊按照權值有小到大順序排列。算法實現過程如下:

 

 

復制代碼
/*
 * 獲取圖中所有邊,並將這些邊按照權值由大到小排序;
 * @param G    函數輸入為圖G
 * @return    返回為ITEM數組,每個元素為EdgeElem
 * 對象,元素按照權值排序;
 */
public static ITEM[] GetEdgeSort(Graph G){
    if(G == null) return null;
    // 首先將所有邊存至ITEM數組
    int ne = G.get_ne();
    int nv = G.get_nv();
    ITEM[] E = new ITEM[ne];
    int edge_cnt = 0;
    for(int i = 0; i < nv; i++){
        for(Edge e = G.firstEdge(i); G.isEdge(e); e = G.nextEdge(e)){
            E[edge_cnt++] = new ITEM(new EdgeElem(e, G.getEdgeWt(e)));
        }
    }
    // 將ITEM數組排序,這里采用快速排序
    Sort st = new Sort();
    st.quicksort(E, 0, E.length - 1);
    // 返回ITEm數組
    return E;
}
復制代碼

 

圖(1~8)逐步實例了Kruskal算法的操作,從不連通的子樹森林逐步演化為一棵樹。邊按其長度的順序添加到MST中,所以組成此森林的頂點相互之間都通過相對短的邊連接。

 

 

 

以上圖為例調用Krustral函數,並跟蹤UnionFind對象中等價類的變化過程。實例程序如下:

 

復制代碼
public class KruskalExample {
    public static void main(String args[]){
        GraphLnk GL = 
            Utilities.BulidGraphLnkFromFile("Graph\\graph7.txt");
        MinimumSpanningTree MSTex = new MinimumSpanningTree(GL);
        MSTex.Krustral();
        MSTex.mst.ford_print_tree();
        System.out.println("MST各條連接邊為:");
        for(int i = 0; i < MSTex.R.length; i++){
            System.out.println(MSTex.R[i]);
        }
    }
}
復制代碼

 

復制代碼
    程序運行結果為:
    
0. -1,    1. -1,    2. -1,    3. -1,    4. -1,    5. -1,    6. -2,    7. 6,    8. -1,    
0. -1,    1. -1,    2. -2,    3. -1,    4. -1,    5. -1,    6. -2,    7. 6,    8. 2,    
0. -1,    1. -1,    2. -2,    3. -1,    4. -1,    5. 6,    6. -2,    7. 6,    8. 2,    
0. -1,    1. -1,    2. 6,    3. -1,    4. -1,    5. 6,    6. -3,    7. 6,    8. 2,    
0. 1,    1. -2,    2. 6,    3. -1,    4. -1,    5. 6,    6. -3,    7. 6,    8. 2,    
0. 1,    1. -2,    2. 6,    3. 6,    4. -1,    5. 6,    6. -3,    7. 6,    8. 6,    
0. 1,    1. 6,    2. 6,    3. 6,    4. -1,    5. 6,    6. -3,    7. 6,    8. 6,    
0. 6,    1. 6,    2. 6,    3. 6,    4. 6,    5. 6,    6. -3,    7. 6,    8. 6,    
MST各條連接邊為:
(6, 7), 1
(2, 8), 2
(6, 5), 2
(5, 2), 4
(1, 0), 4
(3, 2), 7
(0, 7), 8
(3, 4), 9
   
復制代碼

 

結果第一部分為等價類變化過程,第二部分為MST中添加邊的順序。對等價類的顯示結果這里先簡要解釋,詳細分析請參見本節UnitFind部分。以結果中第6行為例進行解釋:

0. 1,1. -2,2. 6,3. 6,4. -1,5. 6,6. -3,7. 6,8. 6,

其中“.”之前前的數值表示頂點號;之后的數值表示了與該頂點屬於同一個等價類的另一頂點,如果該值為負數,表示該頂點為所屬等價類中的根。例如,頂點0對應值為1,而頂點1對應值為-2,則頂點0和1屬於同一個等價類;頂點2, 3, 5, 7, 8對應值都為6,所以頂點2, 3, 5, 6, 7, 8屬於同一個等價類。對應個頂點鏈接圖為圖中(6)。

4. Boruvka算法

Boruvka算法是MST算法中最為古老的算法。類似於Kruskal算法,Bruvka算法也要向MST子樹的森林添加邊來構建MST;但是這是分步完成,每一步都增加多條MST邊。在每一步中,會連接每一棵MST子樹與另一棵子樹的最短邊,再將所有這樣的邊都增加到MST中。

本算法同樣借助於UnionFind類。首先維護一個頂點索引數組,它可為各個MST子樹找出最近的鄰居。其次對圖中每條邊進行以下操作:

l 如果此邊鏈接了同一子樹上的兩個頂點,則將其刪除;

l 否則,檢查此邊所連接的兩個子樹之間的最近鄰居距離,如果有則更新此距離。

遍歷操作圖中所有頂點后,最近鄰居數組中則有了鏈接子樹所需的邊的信息。對於每個頂點索引,要完成一個合並操作(Union)使它與其最近鄰居相連接。在下一步中,去除目的連接的MST子樹中鏈接其他頂點對的所有更長邊。算法實現如下:

 

 

復制代碼
/**
 * Boruvka 算法求解最小支撐樹
 **/
public void Boruvka(){
    int nv = G.get_nv();
    int ne = G.get_ne();

    // 獲取圖上的每一條邊
    EdgeElem[] E = Utilities.GetEdge(G);
    EdgeElem[] B = new EdgeElem[nv];
    // 集合形式的等價類
    UnionFind UF = new UnionFind(nv);
    // 待返回的EdgeElem數組
    R = new EdgeElem[nv]; 
    int r = 0;
    int N = 0;
    // 權值為無窮大的邊
    EdgeElem _z = new EdgeElem(null, 
                        Integer.MAX_VALUE);
    // 對每一個子樹
    for(int e = ne; e != 0; e = N){
        System.out.print("h-\t");
        System.out.println(UF.toString());
        int h, i, j;
        // 權值初始化為 oo
        for(int t = 0; t < nv; t++) 
            B[t] = _z;
        // 對每一條邊
        for(h = 0, N = 0; h < e; h++){
            EdgeElem pe = E[h];
            // 獲取邊的起點和終點
            i = UF.find(pe.get_v1());
            j = UF.find(pe.get_v2());
            // 起點和終點如果在同一個等價類中,則跳出本次循環
            if(i == j) 
                continue;
            // 更新兩棵樹之間的最近距離
            if(pe.get_wt() < B[i].get_wt()) 
                B[i] = pe;
            if(pe.get_wt() < B[j].get_wt()) 
                B[j] = pe;
            // N表示的是當前的子樹個數
            E[N++] = pe;
        }
        // 對MST中每條邊
        for(h = 0; h < nv; h++){
            // B[h]是第h個頂點與其它樹之間的最近距離的邊
            if(B[h] != _z
                // 如果B[h]的起點和終點不在同一個等價類中
                && UF.find(B[h].get_v1()) 
                        != UF.find(B[h].get_v2())){
                    // 將起點和終點放置到同一個等價類中
                    UF.union(B[h].get_v1(), B[h].get_v2());
                    // 並將這條邊添加到EdgeElem數組中
                    R[r++] = B[h];
                }
        }
    }
}
復制代碼

 

函數中,E數組中首先保存途中所有頂點,調用函數GetEdge實現。數組B即為用於各個子樹找出最近的鄰居的頂點索引數組。算法初始狀態下每個頂點分別屬於不同等價類,然后按照上面算法描述中的方法找出各子樹之間的最近連接邊。

圖(1~8)顯示了算法的過程,其中(1~6)為第一次遍歷過程,並得到三個子樹形成的森林;(7~8)為第二次遍歷過程,得到最終的最小支撐樹。算法過程中跟蹤每次遍歷后等價類的變化。

 

以上圖為例,對算法進行測試,實例程序如下:

 

復制代碼
public class BoruvkaExample {
    public static void main(String args[]){
        GraphLnk GL = 
            Utilities.BulidGraphLnkFromFile("Graph\\graph7.txt");
        MinimumSpanningTree MSTex = new MinimumSpanningTree(GL);
        MSTex.Boruvka();
        System.out.println("MST各條連接邊為:");
        for(int i = 0; i < MSTex.R.length; i++){
            System.out.println(MSTex.R[i]);
        }
    }
}
復制代碼

 

復制代碼
    程序結果為:
    
    0. -1,    1. -1,    2. -1,    3. -1,    4. -1,    5. -1,    6. -1,    7. -1,    8. -1,    
    0. -2,    1. 0,    2. -2,    3. 2,    4. 2,    5. -2,    6. 5,    7. 5,    8. 2,    
    0. -3,    1. 0,    2. 0,    3. 2,    4. 2,    5. 0,    6. 5,    7. 5,    8. 2,    
    MST各條連接邊為:
    (0, 1), 4
    (2, 8), 2
    (2, 3), 7
    (3, 4), 9
    (5, 6), 2
    (6, 7), 1
    (0, 7), 8
    (2, 5), 4
復制代碼

 

UnionFind類

UnionFind(合並查找)類可以實現等價類描述。假設一組元素V,其中元素分為n(n ≤ |V|)等價類C1, C2, …, Cn。等價類滿足一下性質:

l 每一個元素只能屬於一個等價類;

l 等價類的同屬關系具有傳遞性。

對於第二個性質,設頂點vi與vj屬於同一等價類,頂點vk與vj屬於同一等價類,則頂點vi和vk與vj都屬於同一等價類。

這里我們關心兩個等價類的合並(Union)和任意元素所屬等價類名稱的返回(Find)。這里我們定義等價類的名稱為等價類中某個特定的元素,由於每個元素只可能屬於一個等價類,所以每個等價類的名稱一定是唯一的,我們稱之為這個等價類的根。合並兩個等價類時,可以分別指定兩個等價類中任意元素,然后將它們合二為一。

類的實現如下:

復制代碼
package Set;

/**
 *  等價關系(等價類)ADT的實現,這里的等價類表示數組中元素的是否是同一類。
 *  數組array表示的是下標序號關聯。
 **/

public class UnionFind {

  private int[] array;

  /**
   *  構造函數,參數為array的長度
   **/
  public UnionFind(int numElements) {
    array = new int [numElements];
    // 數組中數值全為 -1
    for (int i = 0; i < array.length; i++) {
      array[i] = -1;
    }
  }

  /**
   * union() 將兩個集合合並為同一個集合,具體來說是將
   * 第一個集合的下標與第二個集合的下標合並到一個集合中。
   *  @param root1 第一個集合中的任意一個下標.
   *  @param root2 第二個集合中的任意一個下標.
   *  如果root1和root2分別不是兩個集合的根,則首先將他們
   *  的根求出來
   **/
  public void union(int root1, int root2) {
      // 如果root1和root2分別不是兩個集合的根,
      // 則首先將他們的根求出來
      root1 = find(root1);
      root2 = find(root2);
      // 如果root2更高,則將root2設為合並后的根
    if (array[root2] < array[root1]) {
      array[root1] = root2; 
    } 
    else {
      // 如果一樣高,減小其中一個的高度
      if (array[root1] == array[root2]) {
          array[root1]--; 
      }
      // 一樣高或者root1更高,則將root1設為合並后的根
      array[root2] = root1;
    }
  }

  /**
   *  find() 尋找輸入下標所在的集合的集合名
   *  @param x: 輸入下標
   *  @return : 集合名
   **/
  public int find(int x) {
      // x是根,直接返回
    if (array[x] < 0) {
      return x; 
    } 
    else {
      /* 遞歸尋找x的根,在遞歸過程中將壓縮x的索引深度,
       * 使得array[x]中保存根節點的下標*/
      array[x] = find(array[x]);
      return array[x];// 返回根節點
    }
  }
  
  /**
   * 返回所有的等價類
   */
  public String toString(){
      String s = "";
      for(int i= 0; i < array.length; i++){
          s += i+". "+array[i] + ",\t";
      }
      return s;
  }

}
復制代碼

 

其中似有成員為整型數組,數組下標對應實際元素的序號。例如array[i]的值包含第i個元素所屬的等價類。find函數是遞歸函數,充分利用等價類的第二個性質。對於元素i,array[i]=j,包含兩種情況:

l 如果j<0,則說明第i個元素為等價類的根,該等價類的名為i,則返回i;

l 如果j>0,則說明第i個元素與第j個元素屬於同一個等價類,則遞歸調用函數find返回j的等價類名稱,遞歸調用直至滿足上一情況為止。

union函數將兩個等價類合並,只需要分別指定兩個等價類中的任意元素i, j,

l 如果這兩個元素分別是等價類的名,則將array[j]←i;

l 否則,首先調用函數find找到兩個等價類的名稱,然后再合並。

在本節討論的Kruskal算法和Boruvka算法都利用了UnionFind類的union函數和find函數,讀者可自行體會函數的功能。


免責聲明!

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



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