并查集复习笔记

前言

第三篇复习笔记。由于并查集的基本算法比较简单,所以就写两道普通并查集,其他直接上进阶了。

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。

posted @ 2020-11-02 20:06  MontesquieuE  阅读(242)  评论(0编辑  收藏  举报