原文出處:http://blog.sina.com.cn/s/blog_5ec05ef30100cp7f.html
作者:angela
所謂并查集,它是一個集合,這個集合的元素也是集合,他支援三種操作
MakeSet(x),建立一個隻有一個元素x的集合X0,将這個集合放入并查集中;
FindSet(x),在并查集中尋找一個元素S(注意并查集的元素S也是集合) ,滿足
x屬于S; Union(x,y), 将并查集中的元素S1,S2合并,其中x屬于S1,y屬于S2。
下面說說并查集的實作,其實很簡單,用樹來實作。所有屬于同一集合的元素屬于
同一棵樹,這樣我們就可以用數根來表示一個集合,要找到某個元素屬于哪個集合
,隻要找到這個元素所在的樹的樹根;要合并兩個集合,隻要合并兩棵樹。一般比
較友善的方法是用父親數組Parent[]來表示樹,Parent[i]就是節點i的父結點,樹
根的父結點就是它本身。這樣很容易根據某個節點找到他的根,也很容易合并兩棵
樹。但是我們要考慮效率問題。在最壞的情況下,一棵樹退化成一個單連結清單(想象
一下所有的結點隻有唯一的兒子節點),這樣如果連結清單長度為L,從一個節點找到
他的根也需要L次運算,對于一共有n個節點元素的并查集,FindSet平均情況和最
壞情況下的複雜度都是O(n),效率并不高。對于一棵樹而言,如果樹的高度為H,
那麽從葉節點隻要經過H次運算就可以找到樹根,是以我們應該盡量減少樹的高度
,最好的情況是樹的高度隻有1層,這樣就是一個樹根下面有很多兒子,這些兒子
都是樹葉。但是如果兩棵這樣的樹合并,數的高度就會增加,比如高度為H1的樹
T1和高度為H2的樹T2合并,得到的樹的高度有兩種情況: max { H1, H2+1}或者
max{H1+1, H2},前一種情況是T2的根作為T1的根的兒子,後一種情況則是T1的根
作為T2的根的兒子。這就說明經過多次Union操作以後,樹的高度會增加。為了使
得樹的高度盡量地小,我們應該将較矮的樹合并到較高的樹上,是以我們用一個數
rank記錄每棵樹的高度,rank[i]就是以i為根的子樹的高度,在合并的時候就可以
根據rank的值進行合并,Union的代碼如下:
// 合并x,y所在的集合,并傳回交集
int Union(int x, int y)
{
x = FindSet( x ); // 找到x所在的樹的根
y = FindSet( y ); // 找到y所在的樹的根
if( x == y ) return x; // -1表示空集
if( x == -1 ) return y;
if( y == -1 ) return x;
if( Rank[x] > Rank[y] ) { // 将較低的樹合并到較高的樹上
Parent[y] = x;
return x;
}
else {
Parent[x] = y;
if( Rank[x] == Rank[y] ) Rank[y]++; // 改變樹的高度
return y;
}
}
Union其實是一種啟發式算法,rank[i]就是啟發函數值。
另外,為了降低樹的高度,我們在FindSet的時候也會壓縮路徑,比如原來a是樹根
,b是a的兒子,c是b的兒子,d是c的兒子,我們調用FindSet(d)找d所在的樹的樹
根,在找的同時,我們改變該樹的結構,使得調用FindSet(d)結束以後樹的結構變
為a是樹根,b是a的兒子,c也是a的兒子,d也是a的兒子。這就叫做路經壓縮。因
此FindSet代碼如下:
// 傳回到元素i所屬的集合的代表元素, 同時進行路徑壓縮
int FindSet(int i)
{
if( ( i == -1 ) || ( i >= CAPACITY ) ) { // -1表示空集
return -1;
}
else {
if( ( Parent[i] != -1 ) && ( Parent[i] != i ) )
Parent[i] = FindSet( Parent[i] ); // 這句話進行路徑壓縮
return Parent[i];
}
}
并查集的效率很高,可以證明,通過這種樹形結構實作的帶啟發式路徑壓縮的并查
集,FindSet和Union操作的平均代價是O(lgn),這和二分查找的複雜度一緻,可以
證明這已經是理論上最好的算法了,不可能有更好的算法了。如果不進行這種路經
壓縮的話,并查集就退化成了連結清單,在同一連結清單中的元素屬于同一集合,這樣的話
FindSet的複雜度是O(n)。
并查集也是很常用的資料結構,有一道題目“親戚”,也要用并查集:
題目: 親戚(Relations)
或許你并不知道,你的某個朋友是你的親戚。他可能是你的曾祖父的外公的女
婿的外甥女的表姐的孫子。如果能得到完整的家譜,判斷兩個人是否親戚應該是可
行的,但如果兩個人的最近公共祖先與他們相隔好幾代,使得家譜十分龐大,那麼
檢驗親戚關系實非人力所能及。在這種情況下,最好的幫手就是計算機。
為了将問題簡化,你将得到一些親戚關系的資訊,如同Marry和Tom是親戚,
Tom和Ben是親戚,等等。從這些資訊中,你可以推出Marry和Ben是親戚。請寫一個
程式,對于我們的關于親戚關系的提問,以最快的速度給出答案。
參考輸入輸出格式 輸入由兩部分組成。
第一部分以N,M開始。N為問題涉及的人的個數(1 ≤ N ≤ 20000)。這些人的
編号為1,2,3,…,N。下面有M行(1 ≤ M ≤ 1000000),每行有兩個數ai, bi,表示
已知ai和bi是親戚。
第二部分以Q開始。以下Q行有Q個詢問(1 ≤ Q ≤ 1 000 000),每行為ci,
di,表示詢問ci和di是否為親戚。
對于每個詢問ci, di,若ci和di為親戚,則輸出Yes,否則輸出No。
樣例輸入與輸出
輸入relation.in
10 7
2 4
5 7
1 3
8 9
1 2
5 6
2 3
3
3 4
7 10
8 9
輸出relation.out
Yes
No
Yes
如果這道題目不用并查集,而隻用連結清單或數組來存儲集合,那麼效率很低,肯定超
時。