寫在前面:並查集能在一張無向圖中維護節點之間的連通性,這是他的基本用途之一。實際上並查集擅長動態維護許多具有傳遞性的關係。
定義:
在計算機科學中,並查集是一種樹型的數據結構,用於處理一些不交集(Disjoint Sets)的合併及查詢問題
。有一個聯合-查找算法(union-find algorithm)定義了兩個用於此數據結構的操作:
- 確定元素屬於哪一個子集。它可以被用來確定兩個元素是否屬於同一子集。
- 將兩個子集合併成同一個集合。
由於支持這兩種操作,一個不相交集也常被稱爲聯合-查找數據結構(union-find data structure)或合併-查找集合(merge-find set)。
注:並查集並不支持集合的分離和刪除
普通並查集
理解
我們經常會遇到處理兩個集合關係的題目,比如某些集合合併的問題,或者判斷兩個元素是否同在一個集合裏。而並查集其實就是相當於這每個集合中找一個代表元(老大),來表示這個集合,如下圖(網絡圖片)
我們在每個集合裏找一個"老大”做代表,然後查詢的A,B是否在一個集合時,A說我老大是C,B說我老大也是C,那麼A和B就知道他們在同一個集合了;如果A的老大是C,B的老大是D,那麼A、B就不在同一個集合了,要使得A、B所處的集合合併,要麼C成爲D的老大了,要麼D成爲C的老大了。 這就是基本的思想。
實現
我們用一個數組來表示的老大是誰,一開始每個人都是自己的老大。
初始化
int fa[N];
void init(){
for(int i = 1;i <= n;++ i) fa[i] = i;
}
然後,我們查找A、B是否在同一個集合時,是一直找A的老大的老大的… ,B也是同理,知道滿足某個條件,就停下來,找到集合的老大了!比如按上述初始化方式,當時,說明就是集合的老大。比如下圖:(圖片來自網絡
我們查詢是否在一個集合中,
最後發現他們的老大不同唉,說明不在一個集合。而這個過程,是不斷遞歸的找老大的。
int Find(int x){
if(fa[x] == x) return x;
else return Find(fa[x]);
}
可是這樣最壞的情況下使樹鏈,每次查詢都是的,我們介紹兩種優化方法,路徑壓縮和按秩合併
- 路徑壓縮
如果是樹鏈狀,每次詢問A的老大是誰,他就會問老大是誰,就會問老大是誰,這樣下去,一直到有個人說老大是我!這樣傳遞的速度太慢了,而如果這條路徑的人不傻,那麼在老大向A傳老大是我的的路途中,這條路徑的人都知道這個集合真正的老大是誰了,而不用在問自己的老大集合中的老大是誰了,也就是對於路徑中每個,.
這樣就改變了樹的形狀。如下圖(圖片來自網絡
int Find(int x){
if(fa[x] == x) return x;
else return fa[x] = Find(fa[x]);
}
上面我們提到了,當兩個集合進行合併時,其實就是讓兩個集合中一個老大認另一個老大做老大(有點拗口
(圖片來自網絡)
合併A、B的集合,肯定得找來他們各自的老大來說事啊!
void Union(int x,int y){
int q = Find(x);
int p = Find(y);
if(q != p) fa[x] = y;
}
- 按秩合併
你可以理解爲是按各自集合樹的深度合併,在這個算法中,術語“秩”替代了“深度”,秩將不會與高度相同。單元素的樹的秩定義爲,當兩棵秩同爲的樹聯合時,它們的秩。當前有時候我們也常常按照集合的大小進行合併,順便還維護每個集合中元素的數目。
以下兩種都可以用,但是一般題目只用路徑壓縮就夠了。
int Rank[N];//秩初始化爲0
void Union(int x,int y){
int q = Find(x);
int p = Find(y);
if(q != p){
if(Rank[q] > Rank[p]) fa[p] = q;
else if(Rank[q] < Ran[p]) fa[q] = p;
else {
fa[q] = p;
Ran[p] ++;
}
}
}
int Size[N];//初始化爲1
void Union(int x,int y){
int q = Find(x);
int p = Find(y);
if(q != p){
if(Size[q] > Size[p]) fa[p] =q,Size[q] += Size[p];
else fa[q] = p,Size[p] += Size[q];
}
}
Atcoder D - Friend Suggestions(並查集)
題意:
有個人,對雙向的朋友關係,還有對破裂的關係。
現在讓你找的候選朋友,候選朋友是的朋友的朋友(間接朋友關係),並且還不是破裂關係。問每個有多少個候選朋友。
思路:
朋友的傳遞關係顯然可以用並查集進行合併,同時我們用數組維護每個集合的大小。
我們進行找每個人的可能的朋友關係並進行集合的合併。每個集合中的人都互爲朋友。
那麼對於任意點來說,它所在並查集的連通塊中都是它可以通過它的朋友可以到達的,但是這其中也有和它直接是朋友的和有破解關係的朋友。我們用所在集合的大小 和它直接是朋友的有破裂關係的 他自己 。
Codeforces Round #595 (Div. 3)B2
題意:
給你一個數列,其中每個位置 出值爲代表 就是從到的意思。問你每個位置至少需要多少步才能到自身。
思路:
想了一大會兒。。。。每個數字能到自身的話說明會有一個環,那麼環上的點到他們的長度都是這個環的長度。所以我們只要有用並查集合並環是上的點,同時 記錄這個環的大小就行了。
int Size[N];
int fa[N];
int Find(int x) {return fa[x] == -1?x:fa[x] = Find(fa[x]);}
void Union(int x,int y){
int q = Find(x),p = Find(y);
if(Size[q] > Size[p]) swap(q,p);
if(q!=p) {fa[q] = p;Size[p] += Size[q];}
}
int main(){
int t = read();
while(t--){
int n = read();
fill(Size+1,Size+n+1,1);
memset(fa,-1,sizeof fa);
rep(i,1,n){
int a = read();
Union(i,a);
}
rep(i,1,n){
cout << Size[Find(i)]<<' ';
}
puts("");
}
}
AcWing 237. 程序自動分析
題意:
思路:
只有等和不等兩種關係,問你是否會產生矛盾,在線做法還是比較麻煩的,離線做法就好想了,我們先把相等關係的用並查集合併在一起,然後在檢查不等關係是否在一個集合中。
因爲數據範圍比較大,難以放在數組中,所以我們先離散化一下,在處理。
int fa[N];
int x[N],y[N],op[N];
int b[N];
int Find(int x){return fa[x] == 0?x:fa[x] = Find(fa[x]);}
int main(){
int t = read();
while(t--){
memset(fa,0,sizeof fa);
int n = read();
int tot(0);
rep(i,1,n){
x[i] = read(),y[i] = read(),op[i] = read();
b[++tot] = x[i];
b[++tot] = y[i];
}
sort(b+1,b+tot+1);//離散化
int m = unique(b+1,b+tot+1) - b - 1;
rep(i,1,n){
x[i] = lower_bound(b+1,b+m+1,x[i]) - b;
y[i] = lower_bound(b+1,b+m+1,y[i]) - b;
}
rep(i,1,n){
if(op[i] == 1){
int q = Find(x[i]);
int p = Find(y[i]);
if(q!=p) fa[q] = p;
}
}
bool r = 1;
rep(i,1,n){
if(op[i] == 0){
int q = Find(x[i]);
int p = Find(y[i]);
if(q == p) {r = 0;break;}
}
}
if(r) puts("YES");
else puts("NO");
}
}
邊帶權並查集
並查集實際上是由若干棵樹構成的深林,我們可以在樹中的每條邊上記錄一個權值,即維護一個數組,用保存節點到父節點之間的邊權。在每次路徑壓縮後,每個訪問過的節點都會直接指向樹根,如果我們同時更新這些節點的值,就可以利用路徑壓縮過程來統計每個節點到樹根之間的路徑上的一些信息(以根爲中介)
。
考慮到權值就會有以下問題:
- 每個節點都記錄的是與根節點之間的權值,那麼在Find的路徑壓縮過程中,權值也應該做相應的更新,因爲在路徑壓縮之前,每個節點都是與其父節點鏈接着,那個Value自然也是與其父節點之間的權值
- 在兩個並查集做合併的時候,權值也要做相應的更新,因爲兩個並查集的根節點不同
路徑壓縮
int Find(int x){
if(fa[x] == x) return x;
int y = Find(fa[x]);//遞歸找集合代表
d[x] += d[fa[x]];//維護d數組 —— 對邊權求和
return fa[x] = y;//路徑壓縮
}
因爲在路徑壓縮後父節點直接變爲根節點,此時父節點的權值已經是父節點到根節點的權值了,將當前節點的權值加上原本父節點的權值,就得到當前節點到根節點的權值
AcWing 238. 銀河英雄傳說
思路:
M 命令說號戰艦所在列保持原有順序放在號戰艦所在列尾部。
C 命令是查詢是否在同一個序列,是的話輸出之間有多少艘飛船。
很顯然,每個序列就是一條鏈,我們要處理涉及集合的查詢合併問題,不免想到並查集,可是這裏還有關於值的查詢,因爲都是鏈,我們用數組表示之前有多少艘飛船,然後關於值的查詢就等於。
int fa[N];
int d[N];
int Size[N];
int Find(int x){
if(fa[x] == 0) return x;
int y = Find(fa[x]);
d[x] += d[fa[x]];//更新x點到新的根的距離
return fa[x] = y;
}
int main(){
int t = read();
fill(Size+1,Size+t+1,1);
memset(fa,0,sizeof fa);
while(t--){
char c = gc();int u = read(),v = read();
if(c=='M'){
int x = Find(u),y = Find(v);
if(x!=y){
d[x] = Size[y];//x所在列到y所在列的尾部,所以d[x]=Size[y]
Size[y] += Size[x];//y所在列維護多了長度爲Size[x]的列
fa[x] = y;
}
}
else {
int x = Find(u),y = Find(v);
if(x!=y) puts("-1");
else{
cout<<abs(d[u]-d[v])-1<<endl;
}
}
}
}
ACwing 239. 奇偶遊戲
思路:
這個思路真沒想到啊(雖然以前做過一次
這個題目,給你信息,每個區間內可能有偶數個1或者奇數個1。然後讓你輸出最多到哪個位置可以保證前面說的話全是對了。
這個題目有一個很典型的 好的技巧,就是將原問題的區間相關問題,轉化爲了區間端點的問題。
首先我們先分析數據範圍,發現也是數的範圍很大,但是查詢相比卻很少,於是乎我們先保存離線處理,離散化一下。
如果說區間內有偶數個1,那麼爲偶數,根據奇偶性質,的奇偶性應該相同;有奇數個1,那麼和奇偶性應該不同。
我們用表示第和根的奇偶性相同,1表示和根的奇偶性相同。
對於給的每個區間的信息,我們看和是否在同一個集合內,若在,則看和的奇偶性關係是否和給的信息符合;若不在,則合併兩個並查集,注意我們要在兩個並查集的集合代表之間連一條邊,這個邊的權值應該爲什麼呢?
對於給定信息我們不妨設偶數時(因爲前面說兩個數奇偶性相同時爲0),奇數時。那麼。所以
,和上題對比一下,差不多的套路。
int fa[N];
int x[N],y[N];char op[N];
int a[N],tot;
int d[N];
int Find(int x){
if(fa[x] == 0) return x;
int y = Find(fa[x]);
d[x] ^= d[fa[x]];
return fa[x] = y;
}
int main(){
int n = read();
int m = read();
rep(i,1,m){
int u = read(),v = read();
char s[5];
scanf("%s",s);
x[i] = u-1;y[i] = v;
op[i] = s[0];
a[++tot] = u;a[++tot] = v;
}
sort(a+1,a+tot+1);//離散化
int S = unique(a+1,a+tot+1)-a-1;
rep(i,1,m){
x[i] = lower_bound(a+1,a+S+1,x[i]) - a;
y[i] = lower_bound(a+1,a+S+1,y[i]) - a;
}
bool r = 1;
rep(i,1,m){
int q = Find(x[i]),p = Find(y[i]);
if(op[i] == 'e'){
if(q == p){
if(d[x]^[y]!=0) {cout<<i-1;return 0;}
}
else{
d[q] = d[x]^[dy]^0;
fa[q] = p;
}
}
else{
if(q == p){
if(d[x]^d[y]!=1) {cout<<i-1;return 0;}
}
else{
d[q] = d[x]^d[y]^1;
fa[q] = p;
}
}
}
cout<<m;
}
擴展域並查集
按照我的理解,擴展域的並查集是維護一種邏輯上的關係,對於,並不知道它的具體值,但我們拆點拆點來表示的情況,然後根據題意用並查集維護之間的關係。
ACwing 239. 奇偶遊戲
上面用帶邊權的並查集解決的,接下來使用擴展域的並查集解決。
由上面可知,我們區間問題,我們藉助前綴和+奇偶特性轉變爲了處理兩個端點的問題。然後對於這題,我們離散化後拆點建立兩個域,奇數域和偶數域,S爲離散化後的點數,不妨零爲偶數域,爲奇數域,對於給出的信息,若爲偶數,那麼說明同奇偶,我麼合併和,如果爲奇數,說明兩個數奇偶性不同,我們合併和。
當然,每次給出的信息都先檢查一下給的的奇偶性關係是否和域中關係一致。
int x[N],y[N];char op[N];
int a[N],tot;
int fa[N];
int Find(int x){return fa[x] == 0?x:fa[x] = Find(fa[x]);}
int main(){
int n = read();
int m = read();
rep(i,1,m){
x[i] = read()-1;
y[i] = read();
op[i] = gc();
a[++tot] = x[i];
a[++tot] = y[i];
}
sort(a+1,a+tot+1);//離散化
int S = unique(a+1,a+tot+1) - a - 1;
rep(i,1,m){
x[i] = lower_bound(a+1,a+S+1,x[i]) - a;
y[i] = lower_bound(a+1,a+S+1,y[i]) - a;
}
rep(i,1,m){
int q = Find(x[i]),p = Find(y[i]);//偶數域
int w = Find(x[i]+S),e = Find(y[i]+S);//奇數域
if(op[i] == 'e'){
if(w == p) {cout<<i-1;return 0;}
else if(q != p){fa[q] = p;fa[w] = e;}
}
else {
if(q == p) {cout<<i-1;return 0;}
else if(q!=e){fa[q] = e;fa[p] = w;}
}
}
cout<<m;
}
AcWing 240. 食物鏈
思路:
我麼維護三個域,同類域,捕食域,天敵域。
對於
是同類的信息,我們檢查是否有矛盾,即看是否有其他關係,吃或者吃,那麼對於吃y,我們只需看和(y的天敵域)是否是同一類,吃同理。如果沒矛盾,那麼我們將合併。
對於
吃的信息,我們看有沒有矛盾,即是否有其他關係,吃或者和同類。和上面那類一樣一樣處理,那檢查是否同類,只需要檢查他們是否在一個集合。如果沒矛盾,就合併。
int fa[N];
int Rank[N];
int Find(int x){return fa[x] == 0?x:fa[x] = Find(fa[x]);}
void Union(int x,int y){
int q = Find(x),p = Find(y);
if(q != p){
if(Rank[q] > Rank[p]) fa[p] = q;
else if(Rank[q] < Rank[p]) fa[q] = p;
else {
fa[q] = p;
Rank[p]++;
}
}
}
int op,n,k,x,y,q,p,a,b;
int main(){
n = read(),k = read();
int ans = 0 ;
rep(i,1,k){
op = read(),x = read(),y = read();
if(x>n||y>n) {ans++;continue;}
q = Find(x), p = Find(y);
a = Find(x+2*n),b = Find(y+2*n);
if(op == 1){
if(q == b||p == a) ans++;
else {
Union(x,y);
Union(x+n,y+n);
Union(x+2*n,y+2*n);
}
}
else {
if(x == y) {ans ++;continue;}
if(p == a||p == q) ans ++;
else {
Union(x,y+2*n);
Union(x+n,y);
Union(x+2*n,y+n);
}
}
}
cout << ans;
}
我曾想用擴展域並查集解決程序自動分析那道題目,可是一直WA,後來才知道,那道題目不能用,代表和相等的,和不相等,因爲兩個數如果和不等,他們也未必相等!
本文參考諸多資料所寫。--by k
oi-wiki
zhxmdefj
<<算法競賽進階指南>>