並查集複習筆記
並查集複習筆記
前言
第三篇複習筆記。由於並查集的基本演算法比較簡單,所以就寫兩道普通並查集,其他直接上進階了。
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個域,同類域,天敵域,捕食域。
對於矛盾的情況,有三種:
- \(x,y\) 是同類,但是 \(x\) 的捕食域中有 \(y\) (對應條件3)
- \(x,y\) 是同類,但是 \(x\) 的天敵域中有 \(y\) (也是條件3)
- \(x\) 是 \(y\) 的天敵,但是 \(x\) 的天敵域中有 \(y\) (並查集矛盾)
- \(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]銀河英雄傳說
題意
\(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)\)
- \(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.\)
- \(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。