并查集复习笔记
前言
第三篇复习笔记。由于并查集的基本算法比较简单,所以就写两道普通并查集,其他直接上进阶了。
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。