1. 程式人生 > 實用技巧 >並查集複習筆記

並查集複習筆記

並查集複習筆記

前言

第三篇複習筆記。由於並查集的基本演算法比較簡單,所以就寫兩道普通並查集,其他直接上進階了。

0——P3367 【模板】並查集

題目連結

題意

自己看。反正是模板題。

思路

普通並查集。之所以要寫是因為:第一,這道模板沒寫過,板子存檔還是要的。第二,普通並查集有個地方我以前總是寫錯,還錯過完善程式。用錯誤的方式寫帶權會出大問題,普通的話不知道出錯概率如何,反正是錯的。

程式碼

#include <bits/stdc++.h>
using namespace std;
const int N=1e4+10;
int fa[N],n,m;

int find( int x )
{
        return x==fa[x] ? x : fa[x]=find( fa[x] );
}

int main()
{
        scanf( "%d%d",&n,&m );

        for ( int i=1; i<=n; i++ )
                fa[i]=i;
        
        for ( int i=1; i<=m; i++ )
        {
                int opt,x,y; scanf( "%d%d%d",&opt,&x,&y );
                int fx=find( x ),fy=find( y );
                if ( opt==1 )
                {
                        if ( fx!=fy ) fa[fx]=fy;   //以前會寫成 fa[x]=fy;
                }
                else printf( "%c\n",fx==fy ? 'Y' : 'N' );
        }

        return 0;
}

1——P1892 [BOI2003]團伙

題目連結
AcWing 784.
luogu

題意

給定 \(n\) 個人,有兩個種關係,朋友與敵對。朋友的朋友是朋友,敵人的敵人是朋友。兩個人在同一團伙內當且僅當他們是朋友。問最多會有多少個團伙。

思路

非常經典的一道題目,不過還是普通並查集(我有問題)。

具體做法就是:對於朋友的朋友,直接合並即可。

對於敵人的敵人,對每個人存一個“直接敵人”,如果之前沒有出現過直接敵人,那麼就把當前敵人的祖先存到這個數組裡面去;否則把當前敵人和直接敵人合併即可。

關於“最多會有多少個團伙”,可能一開始會沒有思路。其實很顯然,要讓團伙最多,顯然是合併得越少越好,除了維護確定關係之外啥都不幹就好了。

程式碼

#include <bits/stdc++.h>
using namespace std;
const int N=1010;
int n,m,fa[N],en[N];

int find( int x )
{
        return x==fa[x] ? x : fa[x]=find( fa[x] );
}

void merge( int x,int y )
{
        int fx=find( x ),fy=find( y );
        if ( fx==fy ) return;
        fa[fy]=fx;
}

int main()
{
        scanf( "%d%d",&n,&m );
        for ( int i=1; i<=n; i++ )
                fa[i]=i;
        for ( int i=1; i<=m; i++ )
        {
                int x,y; char ch;
                cin>>ch>>x>>y;
                if ( ch=='F' ) merge( x,y );
                else
                {
                        if ( en[x]==0 ) en[x]=find( y );
                        else merge( y,en[x] );
                        if ( en[y]==0 ) en[y]=find( x );
                        else merge( x,en[y] );
                }
        }

        int cnt[N]={0};
        for ( int i=1; i<=n; i++ )
                cnt[find(i)]++;
        int ans=0;
        for ( int i=1; i<=n; i++ )
                if ( cnt[i] ) ans++;
        printf( "%d",ans );
}

2——P2024 [NOI2001]食物鏈

題目連結
luogu
AcWing 240.

題意

有三類動物A,B,C,現有 \(N\) 個動物,以 \(1-N\) 編號。每個動物都是 其中一種。

給出兩種說法:

1 X Y ,表示X和Y是同類。

2 X Y,表示X吃Y。

給出 \(K\) 個說法,有真假。是假話當且僅當滿足三條之一:

1) 當前的話與前面的某些真的話衝突

2) 當前的話中 \(X\)\(Y\)\(N\)

3) 當前的話表示 \(X\)\(X\)

求假話總數。

思路

種類並查集 的典型例題。動物之間的關係有三種:同類,天敵,捕食。

相對應的也就是開3個域,同類域,天敵域,捕食域。

對於矛盾的情況,有三種:

  1. \(x,y\) 是同類,但是 \(x\) 的捕食域中有 \(y\) (對應條件3)
  2. \(x,y\) 是同類,但是 \(x\) 的天敵域中有 \(y\) (也是條件3)
  3. \(x\)\(y\) 的天敵,但是 \(x\) 的天敵域中有 \(y\) (並查集矛盾)
  4. \(x\)\(y\) 的天敵,但是 \(x\) 的同類域中有 \(y\) (條件3)

對於 \(y\)\(x\) 的天敵的情況同理。條件二直接判斷即可,條件1相當於並查集矛盾。

關於實現細節:

我記得當時看秦淮岸神仙的題解,然後學到了一種特別有用的方法,就是把三個域合在一起開,分別為 \(n,n+n,n+n+n\) ,這樣比較好處理。為了避免混淆,最好在程式碼中註釋三段分別對應哪個域。

程式碼

#include <bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int fa[N];		//同類,捕食,天敵
int n,m,k,x,y,ans=0;

int find( int x )
{
        return x==fa[x] ? x : fa[x]=find(fa[x]);
}

void merge( int x,int y )
{
        fa[find(x)]=find(y);
}

int main()
{
        scanf( "%d%d",&n,&m );
        for ( int i=1; i<=3*n; i++ )
                fa[i]=i;
        for ( int i=1; i<=m; i++ )
        {
                int opt,x,y; scanf( "%d%d%d",&opt,&x,&y );
                if ( x>n || y>n ) ans++;
                else if ( opt==1 )
                {
                        if ( find( x )==find( y+n ) || find( x )==find( y+n+n ) ) ans++;	
                        //x,y是同類,但x被y吃,或x是y的天敵 
                        else 
                        {
                                merge( x,y ); merge( x+n,y+n ); merge( x+n+n,y+n+n );
                        }
                }
                else
                {
                        if ( x==y || find(x)==find(y) || find(x)==find(y+n) ) ans++;
                        //x就是y,或xy為同類,或y吃x
                        else
                        {
                                merge( x,y+n+n );       //x及其同類被y吃 
                                merge( x+n,y );         //x的天敵是y 
                                merge( x+n+n,y+n );             //x吃y的天敵 
                        }
                        
                }
        }

        printf( "%d",ans );
}

3——P1196 [NOI2002]銀河英雄傳說

題目連結
luogu
AcWing

題意

\(N\) 列的星際戰場,各列編號為 \(1,2,…,N\)

\(N\) 艘戰艦,依次編號為 \(1,2,…,N\) ,第 \(i\) 號戰艦處於第 \(i\) 列。

\(T\) 條指令,每條指令為以下兩種之一:

1、M i j,讓第 \(i\) 號戰艦所在列的全部戰艦保持原有順序,接在第 \(j\) 號戰艦所在列的尾部。

2、C i j ,詢問第 \(i\) 號戰艦與第 \(j\) 號戰艦當前是否處於同一列中,如果在同一列中,它們之間間隔了多少艘戰艦。

思路

典型的 帶權並查集 。在本題中體現為,多設定兩個個數組,分別記錄 \(i\) 和當前隊伍的隊頭(\(fa[i]\))間隔了多少戰艦,這一整列的戰艦數量是多少(在求無向圖連通塊大小的時候只需要後面這一個陣列,本題比較特殊)。這兩個陣列跟著 find 和 merge 一起維護即可。具體見程式碼。

帶權並查集最典型的應用是求無向圖各個連通塊大小。

程式碼

#include <bits/stdc++.h>
using namespace std;
const int N=30010;
int fa[N],dis[N],siz[N];

int find( int x )
{
        if ( x==fa[x] ) return x;
        int rt=find( fa[x] ); 
        dis[x]+=dis[fa[x]]; 
        return fa[x]=rt;
}

void merge( int x,int y )
{
        x=find( x ); y=find( y );
        fa[x]=y; dis[x]=siz[y]; siz[y]+=siz[x];
}

int main()
{
        int T; scanf( "%d\n",&T );
        for ( int i=1; i<=N-10; i++ )
                fa[i]=i,siz[i]=1;
        
        while ( T-- )
        {
                int a,b; char ch=getchar();  scanf( "%d %d\n",&a,&b );
                if ( ch=='M' ) merge( a,b );
                else
                {
                        if ( find(a)==find(b) ) printf( "%d\n",abs(dis[a]-dis[b])-1 );
                        else printf( "-1\n" );
                }       
        }
}

4——P2391 白雪皚皚

題目連結
luogu

題意

現在有 \(N\) 片雪花排成一列,要對雪花進行 \(M\) 次染色操作,第 \(i\) 次染色操作中,把第 \((i\times p+q)\mod N+1\) 片雪花和第 \((i\times q+p)\mod N+1\) 片雪花之間的雪花(包括端點)染成顏色 \(i\) 。其中 \(p,q\) 是給定的兩個正整數。他想知道最後 \(N\) 片雪花被染成了什麼顏色。

思路

複習的時候新發現的一種題型。用 並查集維護區間染色 ,或者說,並查集維護序列連通性 .

發現每個點染色可能被下一次染色覆蓋,不能直接做,所以首先要將修改反向,保證最後修改的不被覆蓋。

然後對於模擬過程中下一次染哪一個的顏色使用並查集。\(fa[i]\) 表示離i最近,下次修改應該被修改到的點,也就是 \(i\) 之後第一個可以操作的點。

由於每個點最多被修改一次,複雜度 \(O(n)\).

程式碼

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e6+10;
int n,m,p,q,fa[N],col[N];

int find( int x )
{
        return x==fa[x] ? x : fa[x]=find( fa[x] );
}

int main()
{
        scanf( "%d%d%d%d",&n,&m,&p,&q );
        for ( int i=1; i<=n; i++ )
                fa[i]=i;
        
        for ( int i=m; i>=1; i-- )
        {
                int l=(i*p+q)%n+1,r=(i*q+p)%n+1;
                if ( l>r ) swap( l,r );
                for ( int j=r; j>=l; )
                {
                        int fat=find( j );
                        if ( fat==j ) col[j]=i,fa[j]=find( j-1 );               
        //如果找到了應該修改的點,那麼修改,並把“下一個可操作點”指向j-1
        //由於每次往j-1走,所以每個沒有染色的fa是它的右邊一個點,
        //每個染色點的fa經過這樣的路徑壓縮之後就是整個被染色過的區間的右端點。
                        j=fa[j];
        //不斷往可以修改的點跳,直到超出修改範圍
                }
        }

        for ( int i=1; i<=n; i++ )
                printf( "%d\n",col[i] );
}

中場休息

上面都是典例分析。下面就是一些綜合題目了。

5——P1333 瑞瑞的木棍

題目連結
luogu

題意

有一堆的玩具木棍,每根木棍兩端分別被染上了某種顏色。要把這些木棍連在一起拼成一條線,並且使得木棍與木棍相接觸的兩端顏色都是相同的,給出每根木棍兩端的顏色,問是否存在滿足要求的排列方式。

思路

把相同顏色的點看做一個節點,對於每條木棍連邊,那麼題目要求就是判斷是否含有尤拉路(就是經過每條邊恰好一次)。

通常來講,判斷尤拉路都是 dfs,但是並查集也可以做。這裡並查集的作用是判斷圖是連通的。也就是說,如果把 “合併兩個不在一個集合的點” 稱作有效合併,那麼如果圖是連通的,有效合併的次數是 \(n-1\) ,這個可以用並查集維護。

然後對於用字串描述的顏色,套一個 map 或者用 Trie/hash 維護即可。

注:判斷連通圖內是否存在尤拉路的簡單方法是,邊數 \(m\ge n-1\) 且度數為奇數的點個數為 0 或者 2.

程式碼

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N=5e5+10,M=1e7+10;
char s1[12],s2[12];
int tot,cnt,n,num,tr[M][26],pos[M],d[N],fa[N];
int tx[N],ty[N];

int search( char *s )
{
        int len=strlen(s),p=0;
        for ( int i=0; i<len; i++ )
        {
                if ( !tr[p][s[i]-'a'] ) return 0;
                p=tr[p][s[i]-'a'];
        }
        return pos[p];
}

void insert( char *s,int k )
{
        int len=strlen(s),p=0;
        for ( int i=0; i<len; i++ )
        {
                if ( !tr[p][s[i]-'a'] ) tr[p][s[i]-'a']=++cnt;
                p=tr[p][s[i]-'a'];
        }
        pos[p]=k;
}

int find( int x )
{
        return x==fa[x] ? x : fa[x]=find( fa[x] );
}

void merge( int x,int y )
{
        int fx=find( x ),fy=find( y );
        if ( fx==fy ) return; fa[fx]=fy;
}

int main()
{
        int cntn=0;
        while ( scanf( "%s %s",s1,s2 )!=EOF )
        {
                cntn++; int i=cntn;
                tx[i]=search( s1 ); ty[i]=search( s2 );
                if ( tx[i]==0 ) insert( s1,++tot ),tx[i]=tot;
                if ( ty[i]==0 ) insert( s2,++tot ),ty[i]=tot;
                d[tx[i]]++; d[ty[i]]++;
        }
        
        n=cntn;
        for ( int i=1; i<=tot; i++ )
                fa[i]=i;
        for ( int i=1; i<=n; i++ )
                merge( tx[i],ty[i] );
        int fat=find( 1 );
        for ( int i=2; i<=tot; i++ )
                if ( find(i)!=fat ) { printf( "Impossible\n"); return 0; }
        for ( int i=1; i<=tot; i++ )
                if ( d[i]&1 ) num++;
        
        if ( num==0 || num==2 ) printf( "Possible\n" );
        else printf( "Impossible\n" );

        return 0;
}

6——P3631 [APIO2011]方格染色

題目連結
luogu

題意

有一個包含 \(n \times m\) 個方格的表格。要將其中的每個方格都染成紅色或藍色。表格中每個 \(2 \times 2\) 的方形區域都包含奇數個( \(1\) 個或 \(3\) 個)紅色方格。例如,下面是一個合法的表格染色方案(R 代表紅色,B 代表藍色):

B B R B R
R B B B B
R R B R B

表格中的一些方格已經染上了顏色.求給剩下的表格染色,使得符合要求的方案數。

思路

每天一道壓軸好題。 其實這題跟並查集沒啥關係,只是用來維護而已

題意可以簡化為:在 \(n\times m\) 的矩陣中放 01,k 個格子已經放好了,要放滿,且每個 \(2\times 2\) 的格子中有奇數個1.

由題意可知,任意四個格子(二乘二)的異或值為 1,不斷異或相鄰的兩個“矩形”的異或式子 (如:\(A\oplus B\oplus C\oplus D=C\oplus D\oplus E\oplus F=E\oplus F\oplus G\oplus H=1\),選取相鄰的式子得到 \(A\oplus B\oplus E\oplus F=0,A\oplus B\oplus G\oplus H=0\) )

由這個思路推廣,設 \(A(1,1),B(2,1),C(1,j),D(i,1)\)

  1. \(C,D\) 在奇數列上, \(A\oplus B\oplus C\oplus D=0,E\oplus F\oplus G\oplus H=0,=> A\oplus C\oplus F\oplus H=0,A\oplus H=C\oplus F.\)
  2. \(C,D\) 在偶數列上。\(A\oplus B\oplus C\oplus D=1,E\oplus F\oplus G\oplus H=1.\) 此時,當 \(H\) 在偶數行,\(1\oplus A\oplus H=C\oplus F\) ;如果在奇數行,則有 \(A\oplus H=C\oplus F.\)

綜上所述,對於任意 \(H(i,j):\)

如果 \(i|2,j|2\) ,那麼 \(1\oplus (1,1)\oplus (i,j)=(1,j)\oplus (i,1)\) ;否則 \((1,1)\oplus (i,j)=(1,j)\oplus (i,1)\)

這樣就轉化為對 \((1,j),(i,1)\) 的約束。如果 \((1,1)\) 沒有給出,那麼就要列舉兩種情況。

用並查集維護。\(x\oplus y=0\) 時,合併 \((x,y),(x',y')\) ;否則合併 \((x,y'),(x',y)\)。無解特判就是 \(x,x'\in S\) (屬於同一個集合)

合併完成之後得到連通塊個數 \(sum\) ,列舉所有已知點(注意 \((1,1)\) 不算),去掉他們的連通塊,剩下的就是未知個數,\(2^{sum'}\) 即為方案。把 \((1,1)\) 的兩種情況相加即可。

注意:此題由於有虛點( \(x',y'\) ),所以空間要兩倍。

程式碼

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int mod=1e9,N=2e5+10;
int n,m,k,x[N],y[N],z[N],fa[N],g[N];

ll power( ll a,ll b )
{
        ll res=1;
        for ( ; b; b>>=1,a=a*a%mod )
                if ( b&1 ) res=res*a%mod;
        return res;
}

int find( int x )
{
        if ( x==fa[x] ) return x;
        int fat=find( fa[x] ); g[x]^=g[fa[x]]; 
        return fa[x]=fat;
}

int calc( int opt )
{
        for ( int i=1; i<=n+m; i++ )
                fa[i]=i,g[i]=0;
        fa[n+1]=1;
        if ( opt==1 )
                for ( int i=1; i<=k; i++ )
                        if ( x[i]>1 && y[i]>1 ) z[i]^=1;
        for ( int i=1; i<=k; i++ )
        {
                int x=:: x[i],y=:: y[i],z=:: z[i];
                if ( x!=1 || y!=1 )
                {
                        int fx=find(x),fy=find(y+n),ty=g[x]^g[n+y]^z;
                        if ( fx!=fy ) fa[fy]=fx,g[fy]=ty;
                        else if ( ty ) return 0;
                }
        }
        int res=0;
        for ( int i=1; i<=n+m; i++ )
                if ( i==find(i) ) res++;
        return power( 2,res-1 );
}

int main()
{
        scanf( "%d%d%d",&n,&m,&k );
        int flag=-1;
        for ( int i=1; i<=k; i++ )
        {
                scanf( "%d%d%d",&x[i],&y[i],&z[i] );
                if ( (!(x[i]&1)) && (!(y[i]&1)) ) z[i]^=1;
                if ( x[i]==1 && y[i]==1 ) flag=z[i];
        }

        if ( flag!=-1 ) printf( "%d\n",calc( flag ) );
        else printf( "%d\n",(calc(0)+calc(1))%mod );       
}

7——注意事項

  • 帶權並查集往哪裡合併一定要想清楚
  • 合併的時候是兩個祖先合併,不要寫成原來的節點
  • 種類並查集比較複雜,寫的時候一定要想清楚
  • 看到什麼染色,區間修改且覆蓋,維護序列連通的要想到並查集
  • 把上面的題目都寫一遍基本就能學會並查集 並把該犯的錯都犯一遍了

Last

沒有鳴謝,這次是自己寫的qwq。