並查集:
(union-find sets)是一種簡單的用途廣泛的集合. 並查集是若干個不相交集合,能夠實現較快的合併和判斷元素所在集合的操作,應用很多。一般採取樹形結構來存儲並查集,並利用一個rank數組來存儲集合的深度下界,在查找操作時進行路徑壓縮使後續的查找操作加速。這樣優化實現的並查集,空間複雜度爲O(N),建立一個集合的時間複雜度爲O(1),N次合併M查找的時間複雜度爲O(M Alpha(N)),這裏Alpha是Ackerman函數的某個反函數,在很大的範圍內(人類目前觀測到的宇宙範圍估算有10的80次方個原子,這小於前面所說的範圍)這個函數的值可以看成是不大於4的,所以並查集的操作可以看作是線性的。它支持以下三種操作:
-Union (Root1, Root2) //並操作;把子集合Root2併入集合Root1中.要求:Root1和 Root2互不相交,否則不執行操作.
-Find (x) //搜索操作;搜索元素x所在的集合,並返回該集合的名字.
-UFSets (s) //構造函數。將並查集中s個集合初始化爲s個只有一個單元素的子集合.
-對於並查集來說,每個集合用一棵樹表示。
-集合中每個元素的元素名分別存放在樹的結點中,此外,樹的每一個結點還有一個指向其雙親結點的指針。
-設 S1= {0, 6, 7, 8 },S2= { 1, 4, 9 },S3= { 2, 3, 5 }
-爲簡化討論,忽略實際的集合名,僅用表示集合的樹的根來標識集合。
-爲此,採用樹的雙親表示作爲集合存儲表示。集合元素的編號從0到 n-1。其中 n 是最大元素個數。在雙親表示中,第 i 個數組元素代表包含集合元素 i 的樹結點。根結點的雙親爲-1,表示集合中的元素個數。爲了區別雙親指針信息( ≥ 0 ),集合元素個數信息用負數表示。
S1 ∪ S2的可能的表示方法:
void ufsets(int s)
{//構造函數
int i, parent[s];
for(i = 0; i < s; i++){
parent[i] = -1;
}
}
int find(int x)//搜索操作
{
if(parent[x] <= 0){
return x;
}
else{
return find(parent[x]);
}
void union(int root1, int root2){//合併操作
parent[root2] = root1;//root2指向root1
}
Find和Union操作性能不好。假設最初 n 個元素構成 n 棵樹組成的森林,parent[i] = -1。做處理Union(0, 1), Union(1, 2), …, Union(n-2, n-1)後,將產生如圖所示的退化的樹。
執行一次Union操作所需時間是O(1),n-1次Union操作所需時間是O(n)。若再執行Find(0), Find(1), …, Find(n-1), 若被搜索的元素爲i,完成Find(i)操作需要時間爲O(i),完成 n 次搜索需要的總時間將達到
Union操作的加權規則
爲避免產生退化的樹,改進方法是先判斷兩集合中元素的個數,如果以 i 爲根的樹中的結點個數少於以 j 爲根的樹中的結點個數,即parent[i] > parent[j],則讓 j 成爲 i 的雙親,否則,讓i成爲j的雙親。此即Union的加權規則。
parent[0](== -4) < parent[4] (== -3)
void WeightedUnion(int Root1, int Root2) {
//按Union的加權規則改進的算法
int temp = parent[Root1] + parent[Root2];
if ( parent[Root2] < parent[Root1] ) {
parent[Root1] = Root2; //Root2中結點數多
parent[Root2] = temp; //Root1指向Root2
}
else {
parent[Root2] = Root1; //Root1中結點數多
parent[Root1] = temp; //Root2指向Root1
}
}
使用加權規則得到的樹
下面是幾到用並查集可以方便解決的問題:
題目: 親戚(Relations)
或許你並不知道,你的某個朋友是你的親戚。他可能是你的曾祖父的外公的女婿的外甥的表姐的孫子。如果能得到完整的家譜,判斷兩個人是否親戚應該是可行的,但如果兩個人的最近公共祖先與他們相隔好幾代,使得家譜十分龐大,那麼檢驗親戚關係實非人力所能及.在這種情況下,最好的幫手就是計算機。
爲了將問題簡化,你將得到一些親戚關係的信息,如同Marry和Tom是親戚,Tom和B en是親戚,等等。從這些信息中,你可以推出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
如果這道題目不用並查集,而只用鏈表或數組來存儲集合,那麼效率很低,肯定超時。
例程:
#include <stdio.h>
int n, m, q; int pre[20000], rank[20000];
void makeset(int x) { pre[x] = -1; rank[x] = 1; }
void unionone(int a, int b) { int root1 = find(a); int root2 = find(b);
if(rank[root1] > rank[root2]){ pre[root2] = root1; rank[root1]++; } else{ pre[root1] = root2; rank[root2]++; } }
int find(int x) { int r = x, q;
while(pre[r] != -1){ r = pre[r]; } return r; }
int main(int argc, char **argv) { int a, b, c, d, i;
scanf("%d", &n); getchar(); scanf("%d", &m); getchar();
for(i = 0; i < n; i++){ makeset(i); }
for(i = 0; i < m; i++){ scanf("%d %d", &a, &b); getchar(); if(find(a) != find(b)){ unionone(a, b); } }
scanf("%d", &q); getchar();
for(i = 0; i < q; i++){ scanf("%d %d", &c, &d); getchar(); if(find(c) == find(d)){ printf("YES/n"); } else{ printf("NO/n"); } }
return 0; }
|