最小生成樹之克魯斯卡爾(Kruskal)算法

學習最小生成樹算法以前咱們先來了解下 下面這些概念:算法

(Tree):若是一個無向連通圖中不存在迴路,則這種圖稱爲樹。數組

生成樹 (Spanning Tree):無向連通圖G的一個子圖若是是一顆包含G的全部頂點的樹,則該子圖稱爲G的生成樹。網絡

生成樹是連通圖的極小連通子圖。這裏所謂極小是指:若在樹中任意增長一條邊,則將出現一條迴路;若去掉一條邊,將會使之變成非連通圖。數據結構

最小生成樹(Minimum Spanning Tree,MST):或者稱爲最小代價樹Minimum-cost Spanning Tree:對無向連通圖的生成樹,各邊的權值總和稱爲生成樹的權,權最小的生成樹稱爲最小生成樹。ide

構成生成樹的準則有三條:函數

<1> 必須只使用該網絡中的邊來構造最小生成樹。學習

<2> 必須使用且僅使用n-1條邊來鏈接網絡中的n個頂點優化

<3> 不能使用產生迴路的邊。spa

 

構造最小生成樹的算法主要有:克魯斯卡爾(Kruskal)算法和普利姆(Prim)算法他們都遵循以上準則。3d

接下分別討論一下這兩種算法以及斷定最小生成樹是否惟一的方法。

克魯斯卡爾算法

克魯斯卡爾算法的基本思想是以邊爲主導地位,始終選擇當前可用(所選的邊不能構成迴路)的最小權植邊。因此Kruskal算法的第一步是給全部的邊按照從小到大的順序排序。這一步能夠直接使用庫函數qsort或者sort。接下來從小到大依次考察每一條邊(u,v)。

具體實現過程以下:

<1> 設一個有n個頂點的連通網絡爲G(V,E),最初先構造一個只有n個頂點,沒有邊的非連通圖T={V,空},圖中每一個頂點自成一格連通份量。

<2> 在E中選擇一條具備最小權植的邊時,若該邊的兩個頂點落在不一樣的連通份量上,則將此邊加入到T中;不然,即這條邊的兩個頂點落到同一連通份量      上,則將此邊捨去(此後永不選用這條邊),從新選擇一條權植最小的邊。

<3> 如此重複下去,直到全部頂點在同一連通份量上爲止。

下面是僞代碼:

 1 // 把全部邊排序,記第i小的邊爲e[i] (1<=i<=m)m爲邊的個數 
 2 // 初始化MST爲空
 3 // 初始化連通份量,使每一個點各自成爲一個獨立的連通份量
 4 
 5 for (int i = 0; i < m; i++)
 6 {
 7     if (e[i].u和e[i].v不在同一連通份量)
 8     {
 9         // 把邊e[i]加入MST
10         // 合併e[i].u和e[i].v所在的連通份量 
11     } 
12 } 

Kruskal算法動態演示圖

上面的僞代碼,最關鍵的地方在於「連通份量的查詢合併」,須要知道任意兩個點是否在同一連通份量中,還須要合併兩個連通份量。

這個問題正好能夠用並查集完美的解決(不得不佩服前輩們聰明才智啊!)

並查集(Union-Find set)這個數據結構能夠方便快速的解決這個問題。基本的處理思想是:初始時把每一個對象看做是一個單元素集合;而後依次按順序讀入聯通邊,將連通邊中的兩個元素合併。在此過程當中將重複使用一個搜索(Find)運算,肯定一個集合在那個集合中。當讀入一個連通邊(u,v)時,先判斷u和v是否在同一個集合中,若是是則不用合併;若是不是,則用一個合併(Union)運算把u、v所在集合合併,使得這兩個集合中的任意兩個元素都連通。所以並查集在處理時,主要用到搜索合併兩個運算。

爲了方便並查集的描述與實現,一般把前後加入到一個集合中的元素表示成一個樹結構,並用根結點的序號來表示這個集合。所以定義一個parent[n]的數組,parent[i]中存放的就是結點i所在的樹中結點i的父親節點的序號。例如,若是parent[4]=5,就是說4號結點的父親結點是5號結點。約定:若是i的父結點(即parent[i])是負數,則表示結點i就是它所在的集合的根結點,由於集合中沒有結點的序號是負的;而且用負數的絕對值做爲這個集合中所含結點的個數。例如,若是parent[7]=-4,說明7號結點就是它所在集合的根結點,這個集合有四個元素。初始時結點的parent值爲-1(每一個結點都是根結點,只包含它本身一個元素)。

實現Kruskal算法數據結構主要有3個函數。

 1 void UFset() // 初始化 
 2 {
 3     for (int i = 0; i < n; i ++)
 4         parent[i] = -1;
 5 } 
 6 int Find(int x)  // 查找並返回結點x所屬集合的根結點 
 7 {
 8     int s;    // 查找位置 
 9     for (s = x; parent[s]>=0; s = parent[s]);  // 注意這裏的 ; 
10     while (s != x)   // 優化方案 -- 壓縮路徑,使後續的查找 
11     {
12         int tmp = parent[x];
13         parent[x] = s;
14         x = tmp;
15     }
16     return s;
17 }
18 // R1和R2是兩個元素,屬於兩個不一樣的集合,如今合併這兩個集合
19 void Union (int R1, int R2)
20 {
21     // r1位R1的根結點,r2位R2的根結點
22     int r1 = Find(R1), r2 = Find(R2);
23     int tmp = parent[r1] + parent[r2];   // 兩個集合的結點個數之和(負數) 
24     // 若是R2所在樹結點個數 > R1所在樹結點個數
25     // 注意parent[r1]和parent[r2]都是負數
26     if(parent[r1] > parent[r2])    // 優化方案 -- 加權法則 
27     {
28         parent[r1] = r2;        // 將根結點r1所在的樹做爲r2的子樹(合併) 
29         parent[r2] = tmp;       // 跟新根結點r2的parent[]值 
30     }
31     else
32     {
33         parent[r2] = r1;         // 將根結點r2所在的樹做爲r1的子樹(合併) 
34         parent[r1] = tmp;        // 跟新根結點r1的parent[]值 
35     } 
36 } 

接下來對 Find 函數和 Union 函數的實現過程做詳細解釋。

Find 函數:在 Find 函數中若是僅僅靠一個循環來直接獲得結點所屬集合的根結點的話,經過屢次的 Union 操做就會有不少結點在樹的比較深層次中,再查找起來就會很費時。能夠經過壓縮路徑來加快後續的查找速度:增長一個 While 循環,每次都把從結點 x 到集合根結點的路徑上經過的結點直接設置爲根結點的子女結點。雖然這增長了時間,但之後的查找會更快。如圖 3.4 所示,假設從結點 x = 6 開始壓縮路徑,則從結點 6 到根結點1 的路徑上有 3 個結點:六、十、8,壓縮後,這 3 個結點都直接成爲根結點的子女結點,如圖(b)所示。

 

並查集:Find函數中的路徑壓縮

Union 函數:兩個集合並時,任一方可作爲另外一方的子孫。怎樣來處理呢,如今通常採用加權合併,把兩個集合中元素個數少的根結點作爲元素個數多的根結點的子女結點。這樣處理有什麼優點呢?直觀上看,能夠減小樹中的深層元素的個數,減小後續查找時間。

例如,假設從 1 開始到 n,不斷合併第 i 個結點與第 i+1 個結點,採用加權合併思路的過程以下圖所示(各子樹根結點上方的數字爲其 parent[ ]值)。這樣查找任一結點所屬集合的時間複雜度幾乎都是 O(1)!!!

 

並查集:加權合併

不用加權規則可能會獲得下圖所示的結果。這就是典型的退化樹(只有一個葉結點,且每一個非葉結點只有一個子結點)現象,再查找起來就會很費時,例如查找結點 n 的根結點時複雜度爲 O(n)。

 

並查集:合併時不加權的結果。

例 利用 Kruskal 算法求無向網的最小生成樹,並輸出依次選擇的各條邊及最終求得的最小生成樹的權。

假設數據輸入時採用以下的格式進行輸入:首先輸入頂點個數 n 和邊數 m,而後輸入 m 條邊的數據。每條邊的數據格式爲:u v w,分別表示這條邊的兩個頂點及邊上的權值。頂點序號從 1開始計起。

分析:

在下面的代碼中,首先讀入邊的信息,存放到數組 edges[ ]中,並按權值從小到大進行排序。

Kruskal( )函數用於實現 :首先初始化並查集,而後從 edges[ ]數組中依次選用每條邊,若是這條邊的兩個頂點位於同一個連通份量,則要棄用這條邊;不然合併這兩個頂點所在的連通份量。

代碼以下:

 1 #include <stdio.h>
 2 #include <string.h>
 3 #include <algorithm>
 4 #define MAXN 11  //頂點個數的最大值
 5 #define MAXM 20  //邊的個數的最大值
 6 using namespace std; 
 7 
 8 struct edge  //
 9 {
10     int u, v, w; //邊的頂點、權值
11 }edges[MAXM]; //邊的數組
12 
13 int parent[MAXN];  //parent[i]爲頂點 i 所在集合對應的樹中的根結點
14 int n, m;  //頂點個數、邊的個數
15 int i, j;  //循環變量
16 void UFset( )  //初始化
17 {
18     for( i=1; i<=n; i++ ) 
19         parent[i] = -1;
20 }
21 int Find( int x ) //查找並返回節點 x 所屬集合的根結點
22 {
23     int s; //查找位置
24     for( s=x; parent[s]>=0; s=parent[s] );
25     while( s!=x ) //優化方案―壓縮路徑,使後續的查找操做加速。
26     {
27         int tmp = parent[x];
28         parent[x] = s;
29         x = tmp;
30     }
31     return s;
32 }
33 
34 //將兩個不一樣集合的元素進行合併,使兩個集合中任兩個元素都連通
35 void Union( int R1, int R2 )
36 {
37     int r1 = Find(R1), r2 = Find(R2); //r1 爲 R1 的根結點,r2 爲 R2 的根結點
38     int tmp = parent[r1] + parent[r2]; //兩個集合結點個數之和(負數)
39     //若是 R2 所在樹結點個數 > R1 所在樹結點個數(注意 parent[r1]是負數)
40     if( parent[r1] > parent[r2] ) //優化方案――加權法則
41     {
42         parent[r1] = r2; 
43         parent[r2] = tmp;
44     }
45     else
46     {
47         parent[r2] = r1; 
48         parent[r1] = tmp;
49     }
50 }
51 bool cmp( edge a, edge b ) //實現從小到大排序的比較函數
52 {
53     return a.w <= b.w;
54 }
55 void Kruskal( )
56 {
57     int sumweight = 0;  //生成樹的權值
58     int num = 0;  //已選用的邊的數目
59     int u, v;  //選用邊的兩個頂點
60     UFset( ); //初始化 parent[]數組
61     for( i=0; i<m; i++ )
62     {
63         u = edges[i].u; v = edges[i].v;
64         if( Find(u) != Find(v) )
65         {
66             printf( "%d %d %d\n", u, v, edges[i].w );
67             sumweight += edges[i].w; num++;
68             Union( u, v );
69         }
70         if( num>=n-1 ) break;
71     }
72     printf( "weight of MST is %d\n", sumweight );
73 }
74 int main( )
75 {
76     int u, v, w; //邊的起點和終點及權值
77     scanf( "%d%d", &n, &m ); //讀入頂點個數 n
78     for( int i=0; i<m; i++ )
79     {
80     scanf( "%d%d%d", &u, &v, &w ); //讀入邊的起點和終點
81     edges[i].u = u; edges[i].v = v; edges[i].w = w;
82     }
83     sort(edges,edges+m,cmp);
84     Kruskal();
85     return 0;
86 }
View Code
相關文章
相關標籤/搜索