并查集入门
并查集
并查集是一種樹型的高級數據結構,主要用于處理不想交集合的合并及查詢問題。它在計算機科學中有著廣泛的應用。例如求解最小生成樹(克魯斯卡爾算法)、親戚關系的判定、確定無向圖的連通子圖個數、最小公共祖先問題等,都要用到并查集。
集合
集合是數學中最基本的構造之一,將一組滿足某種性質的對象放在一起就形成了集合。集合中包含的對象稱為集合中的元素,集合中的元素是無序而且唯一的。常用大寫英文字母A、B、C等來表示集合,并用x∈A來表示x是集合A中的元素。
集合的并、交、差:
由A、B集合的全體元素組成的集合為A與B的并集,記作A∪B;A與B的公共元素組成的集合稱為A與B的交集,記作A∩B;屬于集合A而不屬于集合B的元素組成的集合稱為A減B的差,記作A-B.
集合中元素的存儲
并查集的概念
在某些應用中,我們要檢查兩個元素是否屬于同一個集合,或者將兩個不同的集合合并為一個集合。這是不相交集合經常處理的兩種操作:查找和合并,我們成為并查集。
查找find:查找一個指定元素屬于哪個集合。對于判斷兩個元素是否屬于同一個集合是非常有用的。
合并union:將兩個集合合并為1個集合。
如何標示一個集合
選擇集合中某個固定的元素作為集合的代表,讓它唯一的標識整個集合。一般來說,選取的代表是任意的。也就是說,到底選擇集合中的哪個元素作為它的代表是無關緊要的。
樹的思想
在并查集中,我們對于集合的表示利用樹的思想,一個集合可以看做一棵樹,樹根即代表該集合的標識。如果兩個集合在同一個樹中,則它們是同一個集合;合并兩個集合,即是對兩棵樹進行合并。
Find(x)
返回元素x所屬集合的代表.
Query(x, y)
詢問元素x和元素y是否在一個集合中。只需判斷find(x)和find(y)是否相等即可。如果相等,說明他們屬于同一個集合,否則它們不屬于同一個集合。
Union(x, y)
將包含元素x的集合(假設為Sx)和包含元素y的集合(假設為Sy)合并為一個新的集合(即這兩個集合的并集),所得到的并集可以用它的任何一個元素來做代表,但在實踐中,一般都是選擇Sx或者Sy的代表作為并集的代表。
N個不同的元素分布在若干個互不相交集合中,需要進行一下3個操作:
并查集操作示例
| Operation | Disjoint sets | |||||
| 初始狀態 | {a} | {b} | {c} | ozvdkddzhkzd | {e} | {f} |
| Merge(a,b) | {a,b} | ? | {c} | ozvdkddzhkzd | {e} | {f} |
| Query(a,c) | False | |||||
| Query(a,b) | True | |||||
| Merge(b,e) | {a,b,e} | ? | {c} | ozvdkddzhkzd | ? | {f} |
| Merge(c,f) | {a,b,e} | ? | {c,f} | ozvdkddzhkzd | ? | ? |
| Query(a,e) | True | |||||
| Query(c,b) | False | |||||
| Merge(b,f) | {a,b,c,e,f} | ? | ? | ozvdkddzhkzd | ? | ? |
| Query(a,e) | True | |||||
| Query(d,e) | False | |||||
土算法
給集合編號
| ? ? | {a} | {b} | {c} | ozvdkddzhkzd | {e} | {f} |
| ? | 1 | 2 | 3 | 4 | 5 | 6 |
| Merge(a,b) | 1 | 1 | 3 | 4 | 5 | 6 |
| Merge(b,e) | 1 | 1 | 3 | 4 | 1 | 6 |
| Merge(c,f) | 1 | 1 | 3 | 4 | 1 | 3 |
| Merge(b,f) | 1 | 1 | 1 | 4 | 1 | 1 |
Query(a,e) :檢查a,e的編號
算法復雜度
Query—O(1); Nerge—O(N)
用樹結構表示集合
Init:
Merge(a,b):
Merge(b,e):
Merge(c,f):
Merge(b,f):
Mege(b,f):
將f所在樹掛在b所在樹的直接子樹
開設父親節點指示數組Par,Par[i]代表第i個元素的父親。若元素i是樹根,則Par[i] = i
Query(b,f)
簡單比較b和f所在的根節點是否相同
缺點
樹可能層次太深,以至于查樹根太慢
Merge(d,c), Merge(c,b), Merge(b,a) …
解決方案一:根據樹的層次進行合并
1、每個節點(元素)維護一個Rank表示子樹最大可能高度
2、較小Rank的樹連到較大Rank的樹的根部
|
|
|
|
|
改進方法二:路徑壓縮
將GET_PAR中查找路徑上的節點直接指向根
|
|
在解決方案二存在的情況下,解決方案一失去了優化效果!
完整代碼:
|
|
|
應用篇
POJ1611 The Suspects
n個學生分屬m個團體,(0 < n <= 30000 ,0 <= m <= 500) 一個學生可以屬于多個團體。一個學生疑似患病,則它所屬的整個團體都疑似患病。已知0號學生疑似患病,以及每個團體都由哪些學生構成,求一共多少個學生疑似患病。
解法:最基礎的并查集,把所有可疑的都并一塊。
Sample Input
100 4
2 1 2
5 10 13 11 12 14
2 0 1
2 99 2
200 2
1 5
5 1 2 3 4 5
1 0
0 0
Sample Output
n4
n1
n1
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | #include <iostream> #include <cstdio> using namespace std; const int MAX = 30000; int n,m,k; int parent[MAX+10]; int total[MAX+10]; //total[GetParent(a)]是a所在的group的人數 int GetParent(int a) { ????//獲取a的根,并把a的父節點改為根 ??? if( parent[a]!= a) ????????parent[a] = ????????????GetParent(parent[a]); ????return parent[a]; } void Merge(int a,int b) { ????int p1 = GetParent(a); ????int p2 = GetParent(b); ????if( p1 == p2 ) ????????return; ????total[p1] += total[p2]; ????parent[p2] = p1; } int main() { ????while(true) { ????????scanf("%d%d",&n,&m); ????????if( n == 0 && m == 0)break; ????????for(int i= 0; i < n; ++i) { ????????????parent[i] = i; ????????????total[i] = 1; ????????} ????????for(int i= 0; i < m; ++i) { ????????????int h,s; ????????????scanf("%d",&k); ????????????scanf("%d",&h); ????????????for( int j = 1; j < k; ++j) { ????????????????scanf("%d",&s); ????????????????Merge(h,s); ????????????} ????????} ????????printf("%d\n",total[GetParent(0)]); ????} ????return 0; } |
POJ 1988 Cube stacking
有N(N<=30,000)堆方塊,開始每堆都是一個方塊。方塊編號1 –N. 有兩種操作:
M x y :表示把方塊x所在的堆,拿起來疊放到y所在的堆上。
C x : 問方塊x下面有多少個方塊。
操作最多有P (P<=100,000)次。對每次C操作,輸出結果。
解法:
除了parent數組,還要開設
sum數組:記錄每堆一共有多少方塊。
若parent[a] = a, 則sum[a]表示a所在的堆的方塊數目。
under數組,under[i]表示第i個方塊下面有多少個方塊。
under數組在堆合并和路徑壓縮的時候都要更新。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | #include <iostream> #include <cstdio> using namespace std; const int MAX = 31000; int parent[MAX]; int sum[MAX]; // 若parent[i]=i,sum[i]表示磚塊i所在堆的磚塊數目 int under[MAX]; // under[i]表示磚塊i下面有多少磚塊 int GetParent(int a) { ????//獲取a的根,并把a的父節點改為根 ??? if( parent[a] == a) ????????return a; ????int t = GetParent(parent[a]); ????under[a] += under[parent[a]]; ????parent[a] = t; ????return parent[a]; } void Merge(int a,int b) { //把b所在的堆,疊放到a所在的堆。 ??? int n; ????int pa = GetParent(a); ????int pb= GetParent(b); ????if( pa == pb)return ; ????parent[pb] = pa; ????under[pb] = sum[pa]; //under[pb] 賦值前一定是0,因為parent[pb] = pb,pb一定是原b所在堆最底下的 ??? sum[pa] += sum[pb]; } int main() { ????int p; ????for(int i= 0; i< MAX; ++ i) { ????????sum[i] = 1; ????????under[i] = 0; ????????parent[i] = i; ????} ????scanf("%d",&p); ????for( int i= 0; i < p; ++ i) { ????????char s[20]; ????????int a,b; ????????scanf("%s",s); ????????if( s[0] == 'M') { ????????????scanf("%d%d",&a,&b); ????????????Merge(b,a); ????????} else { ????????????scanf("%d",&a); ????????????GetParent(a); ????????????printf("%d\n",under[a]); ????????} ????} ????return 0; } |
POJ 1182 食物鏈
三類動物A、B、C,A吃B,B吃C,C吃A。
給出K句話來描述N個動物(各屬于A、B、C三類之一)之間的關系,格式及意義如下:
1 X Y:表示X與Y是同類;
2 X Y:表示X吃Y。
K句話中有真話有假話,當一句話滿足下列三條之一時,這句話就是假話,否則就是真話。1)當前的話與前面的某些真的話沖突,就是假話;2)當前的話中X或Y比N大,就是假話;3)當前的話表示X吃X,就是假話。
求假話的總數。
輸入:
第一行是兩個整數N和K,以一個空格分隔。以下K行每行是三個正整數D,X,Y,兩數之間用一個空格隔開,其中D表示說法的種類。若D=1,則表示X和Y是同類。若D=2,則表示X吃Y。
輸出:
只有一個整數,表示假話的數目。
約束條件:
1 <= N <= 50000,0 <= K <= 100000。
一個容易想到的思路:
用二維數組s存放已知關系:
S[X][Y] = -1:表示X與Y關系未知;
S[X][Y] = 0:表示X與Y是同類;
S[X][Y] = 1:表示X吃Y;
S[X][Y] = 2:表示Y吃X。
對每個讀入的關系s(x,y),檢查S[x][y]:
若S[x][y]=s,則繼續處理下一條;
若S[x][y] = -1,則令S[x][y]=s,并更新S[x][i]、S[i][x]、S[y][i]和S[i][y] (0<i<=n)。
若S[x][y] != s且S[x][y] != -1,計數器加1。
復雜度:
以上算法需要存儲一個N×N的數組,空間復雜度為O(N2)。
對每一條語句
進行關系判定時間為O(1)
加入關系時間為O(N)
總的時間復雜度為O(N*K)
0<=N<=50000,0<=K<=100000,復雜度太高。
進一步分析
對于任意a≠b,a、b屬于題中N個動物的集合S,當且僅當S中存在一個有限序列(P1, P2, …, Pm)(m≥0)使得aP1、P1P2、…、Pm-1Pm、Pmb(或m=0時的ab)之間的相對關系均已確定時,b對a的相對關系才可以確定。
由上面可知,我們不需要保留每對個體之間的關系,只需要為每對已知關系的個體保留一條路徑aP1P2…Pmb(m≥0)其中aP1、P1P2、…、Pm-1Pm、Pmb之間的關系均為已知。兩兩關系已知的動物們,構成一個group
解決方案
使用并查集
用結點表示每個動物,邊表示動物之間的關系。采用父結點表示法,在每個結點中存儲該結點與父結點之間的關系。
parent數組:parent[i]表示i的父節點
relation數組:relation[i]表示i和父節點的關系
初始狀態下,每個結點單獨構成一棵樹。
讀入a,b關系描述時的邏輯判斷:
分別找到兩個結點a、b所在樹的根結點ra、rb,并在此過程中計算a與ra、b與rb之間的相對關系。
若ra!=rb,此句為真話,將a、b之間的關系加入;
若ra=rb,則可計算出r(a,b)=f( r(a,ra) , r(b,rb) )
若讀入的關系與r(a,b)矛盾,則此句為假話,計數器加1;
若讀入的關系與r(a,b)一致,則此句為真話。
一些練習
POJ 2492 A Bug?s Life
法一:深度優先遍歷
每次遍歷記錄下該點是男還是女,
只有: 男->女,女->男滿足,
否則,找到同性戀二分圖匹配,結束程序
法二:并查集
POJ 2524 最基礎的并查集
POJ 1182 并查集的拓展有三類動物A,B,C,這三類動物的食物鏈構成了有趣的環形。A吃B,B吃C,C吃A。也就是說:只有三個group
POJ 1861并查集+自定義排序+貪心求“最小生成樹”
POJ 1703并查集的拓展
POJ 2236 并查集的應用需要注意的地方:1、并查集;2、N的范圍,可以等于1001;3、從N+1行開始,第一個輸入的可以是字符串。
POJ 2560最小生成樹
法一:Prim算法;法二:并查集實現Kruskar算法求最小生成樹
POJ 1456 帶限制的作業排序問題(貪心+并查集)
總結
- 上一篇: 简述二分图
- 下一篇: sql复杂查询语句总结