最小支撐樹樹--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,遍歷與其相鄰的所有邊,並找到權值最小的邊。這一過程實現如下:
* @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,測試程序如下:
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 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]) + ", ");
}
}
}
堆中元素旋轉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 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 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)。這里我們定義等價類的名稱為等價類中某個特定的元素,由於每個元素只可能屬於一個等價類,所以每個等價類的名稱一定是唯一的,我們稱之為這個等價類的根。合並兩個等價類時,可以分別指定兩個等價類中任意元素,然后將它們合二為一。
類的實現如下:
/**
* 等價關系(等價類)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函數,讀者可自行體會函數的功能。