6.3.2 最小支撐樹樹--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 ==  nullreturn;
        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 ==  nullreturn  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子樹找出最近的鄰居。其次對圖中每條邊進行以下操作:

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

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

遍歷操作圖中所有頂點后,最近鄰居數組中則有了鏈接子樹所需的邊的信息。對於每個頂點索引,要完成一個合並操作(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。等價類滿足一下性質:

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

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

對於第二個性質,設頂點vivj屬於同一等價類,頂點vkvj屬於同一等價類,則頂點vivkvj都屬於同一等價類

這里我們關心兩個等價類的合並(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,包含兩種情況:

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

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

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

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

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

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

 


免責聲明!

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



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