並查集總結(不看後悔系列)

寫在前面:並查集能在一張無向圖中維護節點之間的連通性,這是他的基本用途之一。實際上並查集擅長動態維護許多具有傳遞性的關係。

定義:

在計算機科學中,並查集是一種樹型的數據結構,用於處理一些不交集(Disjoint Sets)的合併及查詢問題。有一個聯合-查找算法(union-find algorithm)定義了兩個用於此數據結構的操作:

  • Find:Find: 確定元素屬於哪一個子集。它可以被用來確定兩個元素是否屬於同一子集。
  • UnionUnion:將兩個子集合併成同一個集合。

由於支持這兩種操作,一個不相交集也常被稱爲聯合-查找數據結構(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的老大了。 這就是基本的思想。

實現

我們用一個數組fa[i]fa[i]來表示ii的老大是誰,一開始每個人都是自己的老大。

初始化

int fa[N];
void init(){
    for(int i = 1;i <= n;++ i) fa[i] = i;
}

FindFind

然後,我們查找A、B是否在同一個集合時,是一直找A的老大的老大的… ,B也是同理,知道滿足某個條件,就停下來,找到集合的老大了!比如按上述初始化方式,當fa[x]=xfa[x]=x時,說明xx就是集合的老大。比如下圖:(圖片來自網絡
在這裏插入圖片描述
我們查詢2,52,5是否在一個集合中,
2:2:fa[2]=6,fa[6]=1,fa[1]=1fa[2]=6,fa[6]=1,fa[1]=1
5:5:fa[5]=8,fa[8]=9,fa[9]=9fa[5]=8,fa[8]=9,fa[9]=9

最後發現他們的老大不同唉,說明不在一個集合。而這個過程,是不斷遞歸的找老大的。

int Find(int x){
    if(fa[x] == x) return x;
    else return Find(fa[x]);
}

可是這樣最壞的情況下使樹鏈,每次查詢都是O(N)O(N)的,我們介紹兩種優化方法,路徑壓縮和按秩合併

  • 路徑壓縮
    如果是樹鏈狀,每次詢問A的老大是誰,他就會問fa[A]fa[A]老大是誰,fa[A]fa[A]就會問fa[fa[A]]fa[fa[A]]老大是誰,這樣下去,一直到有個人rootroot說老大是我!這樣傳遞的速度太慢了,而如果這條路徑的人不傻,那麼在老大rootroot向A傳老大是我的的路途中,這條路徑的人都知道這個集合真正的老大是誰了,而不用在問自己的老大集合中的老大是誰了,也就是對於路徑中每個xxfa[x]=rootfa[x]=root.
    這樣就改變了樹的形狀。如下圖(圖片來自網絡
    在這裏插入圖片描述
int Find(int x){
    if(fa[x] == x) return x;
    else return fa[x] = Find(fa[x]);
}

UnionUnion
上面我們提到了,當兩個集合進行合併時,其實就是讓兩個集合中一個老大認另一個老大做老大(有點拗口
(圖片來自網絡)
在這裏插入圖片描述
合併A、B的集合,肯定得找來他們各自的老大來說事啊!

void Union(int x,int y){
    int q = Find(x);
    int p = Find(y);
    if(q != p) fa[x] = y;
}
  • 按秩合併
    你可以理解爲是按各自集合樹的深度合併,在這個算法中,術語“秩”替代了“深度”,秩將不會與高度相同。單元素的樹的秩定義爲0{\displaystyle 0},當兩棵秩同爲r{\displaystyle r}的樹聯合時,它們的秩r+1{\displaystyle r+1}。當前有時候我們也常常按照集合的大小進行合併,順便還維護每個集合中元素的數目。
    以下兩種都可以用,但是一般題目只用路徑壓縮就夠了。
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(並查集)
題意:
nn個人,mm對雙向的朋友關係,還有kk對破裂的關係。
現在讓你找ii的候選朋友,候選朋友是ii的朋友的朋友(間接朋友關係),並且還不是破裂關係。問每個ii有多少個候選朋友。
思路:
朋友的傳遞關係顯然可以用並查集進行合併,同時我們用Size[N]Size[N]數組維護每個集合的大小。
我們進行dfsdfs找每個人的可能的朋友關係並進行集合的合併。每個集合中的人都互爲朋友。
那麼對於任意點ii來說,它所在並查集的連通塊中都是它可以通過它的朋友可以到達的,但是這其中也有和它直接是朋友的和有破解關係的朋友。我們用ii所在集合的大小 - 和它直接是朋友的-有破裂關係的 - 他自己 =ans= ans

Codeforces Round #595 (Div. 3)B2

題意:

給你一個數列,其中每個位置ii 出值爲p[i]p[i]代表i>p[i]i-> p[i] 就是從iip[i]p[i]的意思。問你每個位置至少需要多少步才能到自身。

思路:

想了一大會兒。。。。每個數字能到自身的話說明會有一個環,那麼環上的點到他們的長度都是這個環的長度。所以我們只要有用並查集合並環是上的點,同時SizeSize 記錄這個環的大小就行了。

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");
    }
}

邊帶權並查集

並查集實際上是由若干棵樹構成的深林,我們可以在樹中的每條邊上記錄一個權值,即維護一個數組dd,用d[x]d[x]保存節點xx到父節點fa[x]fa[x]之間的邊權。在每次路徑壓縮後,每個訪問過的節點都會直接指向樹根,如果我們同時更新這些節點的dd值,就可以利用路徑壓縮過程來統計每個節點到樹根之間的路徑上的一些信息(以根爲中介)
考慮到權值就會有以下問題:

  • 每個節點都記錄的是與根節點之間的權值,那麼在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 命令說ii號戰艦所在列保持原有順序放在jj號戰艦所在列尾部。
C 命令是查詢是否在同一個序列,是的話輸出之間有多少艘飛船。
很顯然,每個序列就是一條鏈,我們要處理涉及集合的查詢合併問題,不免想到並查集,可是這裏還有關於值的查詢,因爲都是鏈,我們用數組d[i]d[i]表示ii之前有多少艘飛船,然後關於值的查詢<i,j><i,j>就等於d[i]d[j]1|d[i]-d[j]|-1

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. 奇偶遊戲
在這裏插入圖片描述
思路:
這個思路真沒想到啊(雖然以前做過一次
這個題目,給你mm信息,每個區間內可能有偶數個1或者奇數個1。然後讓你輸出最多到哪個位置可以保證前面說的話全是對了。
這個題目有一個很典型的 好的技巧,就是將原問題的區間相關問題,轉化爲了區間端點的問題。
首先我們先分析數據範圍,發現也是數的範圍很大,但是查詢相比卻很少,於是乎我們先保存離線處理,離散化一下。
如果說[l,r][l,r]區間內有偶數個1,那麼sum[r]sum[l1]sum[r]-sum[l-1]爲偶數,根據奇偶性質,sum[r]sum[l1]sum[r]和sum[l-1]的奇偶性應該相同;有奇數個1,那麼sum[r]sum[r]sum[l1]sum[l-1]奇偶性應該不同。
我們用d[i]=0d[i]=0表示第ii和根的奇偶性相同,1表示和根的奇偶性相同。
對於給的每個區間的信息,我們看l1l-1rr是否在同一個集合內,若在,則看d[l1]d[l-1]d[r]d[r]的奇偶性關係是否和給的信息符合;若不在,則合併兩個並查集,注意我們要在兩個並查集的集合代表之間連一條邊,這個邊的權值應該爲什麼呢?
對於給定信息[l,r,ans][l,r,ans]我們不妨設偶數時ans=0ans=0(因爲前面說兩個數奇偶性相同時爲0),奇數時ans=1ans=1。那麼d[l1]d[r]d[p]=ansd[l-1]\oplus d[r]\oplus d[p]=ans。所以d[p]=d[l1]d[r]ansd[p]=d[l-1]\oplus d[r]\oplus ans
,和上題對比一下,差不多的套路。

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;
}

擴展域並查集

按照我的理解,擴展域的並查集是維護一種邏輯上的關係,對於xx,並不知道它的具體值,但我們拆點拆點來表示xx的情況,然後根據題意用並查集維護x,yx,y之間的關係。

ACwing 239. 奇偶遊戲
上面用帶邊權的並查集解決的,接下來使用擴展域的並查集解決。
由上面可知,我們區間問題,我們藉助前綴和+奇偶特性轉變爲了處理兩個端點的問題。然後對於這題,我們離散化後拆點建立兩個域,奇數域和偶數域,S爲離散化後的點數,不妨零[1,S][1,S]爲偶數域,[S+1,S+S][S+1,S+S]爲奇數域,對於給出的信息[l,r,ans][l,r,ans],若ansans爲偶數,那麼說明l1,rl-1,r同奇偶,我麼合併l1,rl-1,rl1+S,r+Sl-1+S,r+S,如果爲奇數,說明兩個數奇偶性不同,我們合併l1,r+Sl-1,r+Sl1+S,rl-1+S,r
當然,每次給出的信息都先檢查一下給的的奇偶性關係是否和域中關係一致。

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. 食物鏈
在這裏插入圖片描述
思路:
我麼維護三個域,同類域(x,y)(x,y),捕食域(x+n,y+n)(x+n,y+n),天敵域(x+2n,y+2n)(x+2*n,y+2*n)

對於
x,yx,y是同類的信息,我們檢查是否有矛盾,即看是否有其他關係,xxyy或者yyxx,那麼對於xx吃y,我們只需看xxy+2ny+2*n(y的天敵域)是否是同一類,yyxx同理。如果沒矛盾,那麼我們將合併(x,y),(x+n,y+n),(x+2n,y+2n)(x,y),(x+n,y+n),(x+2*n,y+2*n)
對於
xxyy的信息,我們看有沒有矛盾,即是否有其他關係,yyxx或者yyxx同類。yxy吃x和上面那類一樣一樣處理,那檢查xyx、y是否同類,只需要檢查他們是否在一個集合。如果沒矛盾,就合併(x,y+2n),(x+n,y),(x+2n,y+n)(x,y+2*n),(x+n,y),(x+2*n,y+n)

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,後來才知道,那道題目不能用iii+ni+n代表和ii相等的,和ii不相等,因爲兩個數如果和ii不等,他們也未必相等!


本文參考諸多資料所寫。--by k

oi-wiki
zhxmdefj
<<算法競賽進階指南>>

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章