并查集算法总结&专题训练

1.概述

并查集是一种数据结构,用于图论之中(更多时候用于树),通常用来维护一张无向图内 O ( 1 ) O(1) O(1) 判断两个点是否在同一个连通块内,有很多的用法,扩展性也比较高。

2.模板

下面还是通过一道模板讲解并查集的用法。

link

我们假设这 4 个元素分别表示 4 个人。假设每个人都会在一个群内,第 i i i 个人的群主表示为 f a i fa_i fai (其实如果抽象成一棵树,就是 i i i 的父亲节点)

初始时,每一个人单独在一个群内,则令 f a i = i fa_i=i fai=i (这一步是并查集的初始化,非常重要,否则所有人的群主/每个点的祖先就都变成了不存在的 0 号节点,后续操作就会出现很多奇奇怪怪的错误)

看操作 1 :问第 1 个人与第 2 个人在不在同一个群内。

显然不在了,大家各管各的,输出 N

操作 2:合并第 1 个人与第 2 个人所在的群。

如何合并呢?此时两个人分属于不同的群,现在要将两个人合成一个群,那么我们直接把 2 的群主改成 1 不就可以了?即令 f a 2 = 1 fa_2=1 fa2=1 。从树的角度看,初始时每一个点都是单独的根节点,现在在 1 和 2 之间连一条边,生成一棵新树,同时令 1 为根节点。

接下来又问 1 与 2 在不在一个群内。

这一步是并查集的判断操作,判断时我们发现, f a 1 = f a 2 fa_1=fa_2 fa1=fa2 ,那么他们在一个群内,输出 Y

此时群组情况如下所示:

1    3    4 
 \
  2

接下来合并 3 4.仿照上述步骤,令 f a 4 = 3 fa_4=3 fa4=3

这里说明一下,其实令 f a 3 = 4 fa_3=4 fa3=4 也是可以的,看个人习惯,本质上并没有什么区别,毕竟都在一个群里面,谁是群主都没有问题。

群组情况 :

1     3
 \     \
  2     4

下一个操作询问 1 4 在不在一个群内, f a 1 = 1 fa_1=1 fa1=1, f a 3 = 3 fa_3=3 fa3=3 ,群主不一样,不在一个群内,输出 N

下一步合并 2 3,此时。。。。。。

1 不同意了!如果我们修改 f a 3 = 2 fa_3=2 fa3=2 ,没有什么问题(具体为什么见下文),但是万一程序让 f a 2 = 3 fa_2=3 fa2=3 ,那么 1 就不同意了:“ 2 明明在我的群,凭什么到你的群去了?”怎么办?

既然 2 搞不定,我们直接找最高群主 1 谈谈,直接令 f a 1 = 3 fa_1=3 fa1=3 就可以解决了。从树的角度看,就是改变根节点的父亲。

群主情况:

     3
    / \
   1   4
    \
     2

接下来问 1 4 在不在一个群内, f a 1 = f a 4 fa_1=fa_4 fa1=fa4 ,在一个群内,输出 Y

然而此时,如果再来一个询问:询问 2 4在不在一个群内,要怎么办呢?

肉眼可见, 2 4 在一个群内,应该输出 Y ,然而我们上面的判断都是根据 f a i fa_i fai 是否相等判断的,此时并不相等,不就出问题了吗?

为了解决这个问题,方法是:找到最高群主也就是根节点。

看图,2 的群主是 1 ,而 1 的群主是 3 ,这样 2 的最高群主不就是 3 了吗?4 的最高群主也是 3 ,在同一个群内。

如果此时又来一个问题:判断现在有几个群要怎么办呢?

由于每一个群都有最高群主,且只有最高群主的群主是自己(为什么?),那么只要统计出有几个 i ∈ [ 1 , n ] i \in [1,n] i[1,n] 使得 f a i = = i fa_i==i fai==i 即可。也就是找出每一棵树的根节点。

完美解决~~~

代码实现:

  1. 初始化:
    这里直接一遍 for 即可。
    for(int i=1;i<=n;i++) fa[i]=i;
    
  2. 查找某个节点的最高群主也就是根节点。
    递归查找即可,代码如下:
    int gf(int x) {
          
          return (fa[x]==x)?x:gf(fa[x]);}
    
    其中, f a x = = x fa_x==x fax==x 表示找到根节点了(根节点的父亲就是根节点,初始化时已经这样操作过了),找到返回 x x x ,否则递归查找。
    然而你以为这样就结束了吗?看下图:
    1-2-3-4-5-6-7-......-op(某极大的数字,比如说 1e5 1e6 之类的)
    
    这样,查询一次 f a o p fa_{op} faop 就要 O ( 1 e 5 或 1 e 6 ) O(1e5或1e6) O(1e51e6) 的时间复杂度,多查几次不就 TLE 了?为了解决这个问题,我们引入一个优化:路径压缩。
    路径压缩的目的就是为了解决上面的问题,即在查找某节点的祖先的时候,我们将一路上查找的所有节点的父亲全部连到根节点,也就是变成下图:
    1
    | \ \ \   \
    2  3 4 ... op
    
    这样,查询复杂度直接降至 O ( 1 ) O(1) O(1) ,大大优化查询复杂度。
    而代码只需要这样改:
    int gf(int x) {
          
          return (fa[x]==x)?x:fa[x]=gf(fa[x])}
    
  3. 合并操作:
    找到根节点合并即可。
    void hb(int x,int y) {
          
          if(gf(x)!=gf(y)) fa[fa[x]]=fa[y];}
    //由于加入了路径压缩所以不会有问题。
    
  4. 查询操作
    判断两个点的祖先是否相同即可。
    cout<<((gf(x)==gf(y))?'Y':'N')<<"\n";
    //实测这里三目运算符外面不加括号会CE
    
  5. 统计树的数量
    根据上述所讲,一遍 for 即可。
    for(int i=1;i<=n;i++) if(fa[i]==i) ans++;
    

代码:

#include<bits/stdc++.h>
using namespace std;

const int MAXN=1e4+10;
int n,m,fa[MAXN];

int gf(int x) {
    
    return (fa[x]==x)?x:fa[x]=gf(fa[x]);}
void hb(int x,int y) {
    
    if(gf(x)!=gf(y)) fa[fa[x]]=fa[y];}

int read()
{
    
    
	int sum=0,fh=1;char ch=getchar();
	while(ch<'0'||ch>'9') {
    
    if(fh=='-') fh=-1;ch=getchar();}
	while(ch>='0'&&ch<='9') {
    
    sum=(sum<<3)+(sum<<1)+ch-'0';ch=getchar();}
	return sum*fh;
}

int main()
{
    
    
	n=read();m=read();
	for(int i=1;i<=n;i++) fa[i]=i;
	for(int i=1;i<=m;i++)
	{
    
    
		int x,y,z;
		z=read();x=read();y=read();
		if(z==1) hb(x,y);
		else cout<<((gf(x)==gf(y))?'Y':'N')<<"\n";
	}
	return 0;
}

如果你看懂了上述代码,那么恭喜你,学会了并查集的基础操作!

接下来,你将会见到各路例题以及并查集的各种神奇用法。

3.例题

题单:

1.入门题:

[BOI2003]团伙

这道题是一道练手题,思维与算法难度都不高,就是一个并查集。

首先处理读入数据,将是朋友的人合并,是敌人的人先存在 v v v 数组里面(使用 vector ,不会的请自行查百度)。

然后根据我的敌人的敌人是我的朋友,三重循环再合并一次即可。

代码(篇幅有限,只放部分代码,下同):

const int MAXN=1000+10;
int n,m,ans,fa[MAXN];
vector<int>v[MAXN];

int main()
{
    
    
	n=read();m=read();
	for(int i=1;i<=n;i++) fa[i]=i;
	for(int i=1;i<=m;i++)
	{
    
    
		char op;int p,q;
		cin>>op;p=read();q=read();
		if(op=='F') hb(p,q);
		else
		{
    
    
			v[p].push_back(q);
			v[q].push_back(p);
		}
	}
	for(int i=1;i<=n;i++)
		for(int j=0;j<v[i].size();j++)
			for(int k=0;k<v[v[i][j]].size();k++) hb(i,v[v[i][j]][k]);
	for(int i=1;i<=n;i++) if(gf(i)==i) ans++;
	cout<<ans<<"\n";
	return 0;
}

2.与别的算法结合:

搭配购买

卖云朵可还行

这道题首先,要同时买两朵云的操作就很像并查集,因此我们可以考虑使用并查集来求解(通常题目当中出现了 “同时…” / “一起…” 等字眼都有可能是并查集)。

然后,又看到要买云朵,每种云朵只有一份,钱数又是有限的,浓浓的透露出 0/1 背包 的气息。

因此,本道题的算法为:并查集 + 0/1 背包

首先将必须同时购买的物品合并,然后将云朵组成的一棵棵树中所有节点的 c i , d i c_i,d_i ci,di 全部加起来,放到新数组 m o n e y j , v a l u e j money_j,value_j moneyj,valuej 中,跑一遍 0/1 背包即可求解。

代码:

const int MAXN=1e4+10;
int n,m,w,c[MAXN],d[MAXN],money[MAXN],value[MAXN],fa[MAXN],ys[MAXN],tmp,f[MAXN];

int main()
{
    
    
	n=read();m=read();w=read();
	for(int i=1;i<=n;i++) {
    
    c[i]=read();d[i]=read();fa[i]=i;}
	for(int i=1;i<=m;i++)
	{
    
    
		int x,y;
		x=read();y=read();
		hb(x,y);
	}//合并操作
	for(int i=1;i<=n;i++) if(gf(i)==i) ys[i]=++tmp;//处理出最后物品个数
	for(int i=1;i<=n;i++)
	{
    
    
		money[ys[fa[i]]]+=c[i];
		value[ys[fa[i]]]+=d[i];
	}//算出 money[i] 和 value[i]
	for(int i=1;i<=tmp;i++)
		for(int j=w;j>=money[i];j--)
			f[j]=Max(f[j],f[j-money[i]]+value[i]);// 0/1 背包
	cout<<f[w]<<"\n";
	return 0;
}

关押罪犯

这道题可以使用二分图来解,那么如何使用并查集来解呢?

由于要想办法让最大值最小,所以使用二分?

No,这道题不需要使用二分,而是贪心即可。想一想,我们只需要尽量将怒气值大的罪犯组拆掉不就好了,碰到第一个不能拆掉的就是答案。

因此,这道题的算法为:并查集 + 贪心。

首先,按照怒气值从大到小排序一遍。

然后,我们令 d i d_i di 表示 i i i 的第一个会与他发生摩擦的人,初始化为 0 。

接下来处理数据。假设此时我们要处理 a a a b b b 发生摩擦,怒气值为 c c c 的信息:

  1. 如果此时 a , b a,b a,b 已经在一起了,直接输出 c c c ,结束程序。
  2. 否则,他们不在一起,以 a a a 为例:如果 d a = 0 d_a=0 da=0,说明此时没有人与他有摩擦,则 d a = b d_a=b da=b ,否则说明已经有人与他有摩擦了,由于只有两个监狱,那么合并 b , d a b,d_a b,da 即可。
  3. 正确性:显然要将 d a , a d_a,a da,a 拆掉。假设 b , d a b,d_a b,da 之间的怒气值(如果没有摩擦为 0)为 c ′ c' c ,根据之前的排序,必然有 c ′ < c c'<c c<c,那么显然合并 b , d a b,d_a b,da 比合并 a , b a,b a,b 更优。

代码:

const int MAXN=20000+10,MAXM=100000+10;
int n,m,fa[MAXN],d[MAXN];
struct node
{
    
    
	int a,b,c;
}a[MAXM];

int main()
{
    
    
	n=read();m=read();
	for(int i=1;i<=m;i++) {
    
    a[i].a=read();a[i].b=read();a[i].c=read();}
	for(int i=1;i<=n;i++) fa[i]=i;
	sort(a+1,a+m+1,cmp);//自行打 cmp 函数
	for(int i=1;i<=m;i++)
	{
    
    
		if(gf(a[i].a)!=gf(a[i].b))
		{
    
    
			if(!d[a[i].a]) d[a[i].a]=a[i].b;
			else hb(d[a[i].a],a[i].b);
			if(!d[a[i].b]) d[a[i].b]=a[i].a;
			else hb(d[a[i].b],a[i].a);
		}
		else {
    
    cout<<a[i].c<<"\n";return 0;}
	}
	cout<<"0\n";
	return 0;
}

3.考思维的题:

[JSOI2008]星球大战

正常的并查集支持合并操作,但是不支持删除操作,然而这道题的所有操作不是合并就是删除,那么要怎么办呢?

既然并查集支持合并操作,那么我们想办法支持合并操作就好了呗!

我们将打击星球的顺序 倒过来操作 ,将其视作 重建星球 ,然后每重建一个合并一次,最后处理连通块个数不就好了qwq。

关于如何处理连通块个数,这里提供一个 O ( 1 ) O(1) O(1) 的思想:

  1. 初始化 s u m = n sum=n sum=n ,表示有 n n n 个连通块。
  2. 每合并两个点, s u m − − sum-- sum
  3. 注意合并的两个点不能在同一个连通块内。

注意:最后输出答案时要逆序输出,没有重建的星球不算在答案内,只有当两个星球全部重建完成才能合并。这里我使用 b o o k book book 数组标记是否合并完成。

代码:

const int MAXN=4e5+10;
int n,fa[MAXN],m,k,des[MAXN],ans[MAXN],sum;
bool book[MAXN];
vector<int>v[MAXN];

void hb(int x,int y) {
    
    if(gf(x)!=gf(y)) {
    
    sum--;fa[fa[x]]=fa[y];}}
//注意 sum--;,gf(x)略

int main()
{
    
    
	n=read();m=read();
	for(int i=0;i<n;i++) fa[i]=i;
	for(int i=1;i<=m;i++)
	{
    
    
		int x=read();int y=read();
		v[x].push_back(y);v[y].push_back(x);
	}
	k=read();sum=n;
	for(int i=1;i<=k;i++) book[des[i]=read()]=1;
	for(int i=0;i<n;i++)
		if(!book[i])
			for(int j=0;j<v[i].size();j++)
				if(!book[v[i][j]]) hb(i,v[i][j]);
	for(int zzh=k;zzh>=1;zzh--)
	{
    
    
		ans[zzh]=sum-zzh;
		book[des[zzh]]=0;
		for(int i=0;i<v[des[zzh]].size();i++)
			if(!book[v[des[zzh]][i]]) hb(v[des[zzh]][i],des[zzh]);
	}
	ans[0]=sum;
	for(int i=0;i<=k;i++) cout<<ans[i]<<"\n";
	return 0;
}

[IOI2014]game 游戏

别看这道题是 IOI 的题目,其实想通了真的非常简单。你看评级都是绿色

首先,为了让梅玉只有到最后一个询问才能判断是否连通,这里就有一种思路:我们构造某一张图使得这张图连通,且最后一个询问问的点 x , y x,y x,y 会连一条边,这里记作 x − > y x->y x>y ,但是一旦我们删去了 x − > y x->y x>y 整张图就会裂成两个集合,换句话说, x − > y x->y x>y 是这一张图的桥/割边。(桥/割边的定义:如果删除某条 u − > v u->v u>v 的边后途中连通块个数增加,那么 x − > y x->y x>y 是这张图的桥/割边)

为什么正确呢?如果 x − > y x->y x>y 是这张图的割边,那么在倒数第二个询问中梅玉依然不能判断整张图是否连通(有 2 个连通块),此时她必须再问一次才能确定图是否连通。

因此思路就很明确了,我们对于某个询问 a − > b a->b a>b ,如果合并 a , b a,b a,b 以后 x , y x,y x,y 在一个连通块内,这显然不是我们想要的操作,此时不能合并 a , b a,b a,b ;否则,合并 a , b a,b a,b 。最后不要忘记输出最后一条边是否连通,这里输出 01 都可以。不过根据我们构造的方案,最好输出 1。(实测 0 也能够通过)

当然,本博客只是提供了其中一种思路,具体别的思路也请各位发现然后解决。

所以你看,IOI的题也不见得非常难

代码:

const int MAXN=1500+10;
int n,m,fa[MAXN],q1[MAXN*MAXN],q2[MAXN*MAXN];

int main()
{
    
    
	n=read();m=n*(n-1)/2;
	for(int i=1;i<=m;i++) {
    
    q1[i]=read();q2[i]=read();}
	for(int i=1;i<=n;i++) fa[i]=i;
	for(int i=1;i<m;i++)
	{
    
    
		int x=q1[i],y=q2[i];
		int fx=gf(x),fy=gf(y);
		int x1=q1[m],y1=q2[m];
		int fx1=gf(x1),fy1=gf(y1);
		if(fx>fy) swap(fx,fy);
		if(fx1>fy1) swap(fx1,fy1);
		if(fx==fx1&&fy==fy1) cout<<"0\n";//判断 最后两个点 合并之后是否在同一集合内,在就不合并
		else
		{
    
    
			cout<<"1\n";
			hb(x,y);
		}//否则合并
	}
	cout<<"1\n";//输出 1 也可以
	return 0;
}

4.二维转一维:

[USACO14JAN]Ski Course Rating G

这道题也是一道与贪心相结合的题目。

首先,我们需要将二维的地图转成一维:对于 ( i , j ) (i,j) (i,j) (第 i i i 行第 j j j 列,下同),我们将其在一维编号为 ( i − 1 ) ∗ m + j (i-1)*m+j (i1)m+j (注意不是 n n n),然后对于相邻的两个二维的点连一条边,将点压成一维后按照边权从小到大排序。

然后,对于每一条边:

  1. 首先,如果这条边连着的两个点在一个连通块内, continue;
  2. 然后,如果两棵子树 s i z e size size 和大于等于 t t t,那么 a n s + = c ∗ ( c n t i ) ans+=c*(cnt_i) ans+=c(cnti) c n t i cnt_i cnti 见下文)。 其中, i i i 为两个子树的编号,且 s i z e i < t size_i<t sizei<t 。为什么大于 t t t 的就不能统计了呢?因为之前 s i z e i < t size_i<t sizei<t 的时候已经统计过一次,此时又统计就会造成浪费,并且即使有新的起点加入,也已经被统计过,没有意义了。
  3. 而后合并两个点,如果这两个点内有起点,我们就将增加的起点个数存在 c n t i cnt_i cnti 里面。

代码:

const int MAXN=500+10;
int t,n,m,a[MAXN][MAXN],b[MAXN][MAXN],fa[MAXN*MAXN],size[MAXN*MAXN],tmp,cnt[MAXN*MAXN];
typedef long long LL;
LL ans;//不开long long见祖宗
struct node
{
    
    
	int a,b,c;
}dis[2*MAXN*MAXN];

int main()
{
    
    
	//读入略,a=地图,b=是否为起点
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
		{
    
    
			if(j!=m)
			{
    
    
				tmp++;
				dis[tmp].a=turn(i,j);dis[tmp].b=turn(i,j+1);dis[tmp].c=abs(a[i][j]-a[i][j+1]);
			}
			if(i!=n)
			{
    
    
				tmp++;
				dis[tmp].a=turn(i,j);dis[tmp].b=turn(i+1,j);dis[tmp].c=abs(a[i][j]-a[i+1][j]);
			}
			if(b[i][j]==1) cnt[turn(i,j)]=1;
		}//连边操作
	for(int i=1;i<=n*m;i++) fa[i]=i,size[i]=1;
	sort(dis+1,dis+tmp+1,cmp);
	for(int i=1;i<=tmp;i++)
	{
    
    
		int fx=gf(dis[i].a),fy=gf(dis[i].b);
		if(fx==fy) continue;
		if(size[fy]+size[fx]>=t)
		{
    
    
			if(size[fy]<t) ans+=(LL)dis[i].c*cnt[fy];
			if(size[fx]<t) ans+=(LL)dis[i].c*cnt[fx];
		}
		if(size[fx]>size[fy]) swap(fx,fy);
		fa[fx]=fy;
		size[fy]+=size[fx];cnt[fy]+=cnt[fx];//注意更新答案
	}
	cout<<ans<<"\n";
	return 0;
}

小 D 的地下温泉

这道题类似,首先二维转一维不说,然后如果两个相邻点都是泉水合并。

询问操作:直接求出询问点所在树的 s i z e size size 即可,求个最大值。

有个坑点:当心所有点都是土地,此时我们需要输出 1

修改操作:泉水改土地直接修改地图然后 s i z e − − size-- size 即可。土地改泉水时我们需要新开一个点,改变地图后令新开的点 f a = 自 己 , s i z e = 1 fa=自己,size=1 fa=,size=1 ,然后四周合并一遍即可。注意不能直接在原点上修改,否则会有很多奇奇怪怪的问题。

代码:

const int MAXN=1e6+10;
int n,m,fa[MAXN<<1],size[MAXN<<1],q,ys[MAXN<<1],tmp;
int Next[4][2]={
    
    {
    
    0,1},{
    
    1,0},{
    
    0,-1},{
    
    -1,0}};
char a[MAXN];
//gf(),turn()略
void hb(int x,int y) {
    
    if(gf(x)!=gf(y)) {
    
    if(size[fa[y]]>size[fa[x]]) swap(x,y);size[fa[y]]+=size[fa[x]];fa[fa[x]]=fa[y];}}

int main()
{
    
    
	n=read();m=read();tmp=n*m;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			cin>>a[turn(i,j)];
	for(int i=1;i<=n*m;i++) {
    
    fa[i]=i;size[i]=((a[i]=='.')?1:0);}
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			ys[turn(i,j)]=turn(i,j);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
		{
    
    
			if(i!=1)
			{
    
    
				if(a[turn(i,j)]=='.'&&a[turn(i-1,j)]=='.') hb(turn(i,j),turn(i-1,j));
			}
			if(j!=1)
			{
    
    
				if(a[turn(i,j)]=='.'&&a[turn(i,j-1)]=='.') hb(turn(i,j),turn(i,j-1));
			}
		}
	q=read();
	for(int i=1;i<=q;i++)
	{
    
    
		int op,w;
		op=read();w=read();
		if(op==1)
		{
    
    
			int flag=1,ans=0;
			for(int j=1;j<=w;j++)
			{
    
    
				int x,y;
				x=read();y=read();
				if(a[turn(x,y)]=='.'&&size[gf(ys[turn(x,y)])]>ans)
				{
    
    
					ans=size[gf(ys[turn(x,y)])];
					flag=j;
				}
			}
			cout<<flag<<"\n";
		}
		else
		{
    
    
			for(int j=1;j<=w;j++)
			{
    
    
				int x,y;
				x=read();y=read();
				if(a[turn(x,y)]=='.')
				{
    
    
					a[turn(x,y)]='*';
					size[gf(ys[turn(x,y)])]--;
				}
				else
				{
    
    
					ys[turn(x,y)]=++tmp;
					a[turn(x,y)]='.';fa[ys[turn(x,y)]]=ys[turn(x,y)];size[ys[turn(x,y)]]=1;
					for(int k=0;k<4;k++)
					{
    
    
						int tx=x+Next[k][0];
						int ty=y+Next[k][1];
						if(tx>0&&ty>0&&tx<=n&&ty<=m&&a[turn(tx,ty)]=='.') hb(ys[turn(x,y)],ys[turn(tx,ty)]);
					}
				}
			}
		}
	}
	return 0;
}

5.扩展域并查集&边带权并查集:

[NOI2001]食物链

这道题我是用扩展域求解的,各位读者可以尝试使用边带权求解 (其实是我不会)

扩展域的原理:扩大并查集的上限来满足题目需要。

这道题,我们扩大并查集上线至 3 ∗ n 3*n 3n ,由于不知道哪个动物在哪个组,令 1... n 1...n 1...n , n + 1...2 ∗ n n+1...2*n n+1...2n , 2 ∗ n + 1...3 ∗ n 2*n+1...3*n 2n+1...3n为三个组, x , x + n , x + 2 ∗ n x,x+n,x+2*n x,x+n,x+2n 表示同一个动物,如果是组内元素同祖先,表示他们是同类关系;如果是跨组同祖先,表示捕食关系,本题规定如果 g f ( x ) = = g f ( y + n ) ∣ ∣ g f ( x + n ) = = g f ( y + n + n ) ∣ ∣ g f ( x + n + n ) = = g f ( y ) gf(x)==gf(y+n)||gf(x+n)==gf(y+n+n)||gf(x+n+n)==gf(y) gf(x)==gf(y+n)gf(x+n)==gf(y+n+n)gf(x+n+n)==gf(y) 那么 x x x y y y

如何判定一句话与前面的真话是矛盾的呢?

如果一句话告诉你 x , y x,y x,y 是同类,但是事实是 x x x y y y 或者 y y y x x x ,那么是假的,否则是真的,合并 ( x , y ) (x,y) (x,y), ( x + n , y + n ) (x+n,y+n) (x+n,y+n), ( x + n + n , y + n + n ) (x+n+n,y+n+n) (x+n+n,y+n+n)。注意都要合并,否则传递不及时可能会导致一些错误。

如果一句话告诉你 x x x y y y ,但是事实是 x , y x,y x,y 是同类或者 y y y x x x ,那么是假的,否则是真的,合并 ( x , y + n ) (x,y+n) (x,y+n), ( x + n , y + n + n ) (x+n,y+n+n) (x+n,y+n+n), ( x + n + n , y ) (x+n+n,y) (x+n+n,y)

然后就做完了。如果实在看不懂我的题解,还可以看一看 luogu 题目里面的题解,或许能够更好的理解。

代码:

const int MAXN=5e4+10;
int n,k,fa[MAXN*3],ans=0;

int main()
{
    
    
	n=read();k=read();
	for(int i=1;i<=n*3;i++) fa[i]=i;
	for(int i=1;i<=k;i++)
	{
    
    
		int op,x,y;
		op=read();x=read();y=read();
		if(x>n||y>n) ans++;
		else if(op==2&&x==y) ans++;
		else
		{
    
    
			if(op==1)
			{
    
    
				if(gf(x)==gf(y+n)||gf(x+n)==gf(y)) ans++;
				else hb(x,y),hb(x+n,y+n),hb(x+n+n,y+n+n);
			}
			else
			{
    
    
				if(gf(x)==gf(y)||gf(y)==gf(x+n)) ans++;
				else hb(x,y+n),hb(x+n,y+n+n),hb(x+n+n,y);
			}
		}
	}
	cout<<ans<<"\n";
	return 0;
}

[NOI2002]银河英雄传说

这道题使用边带权并查集来做。注意这一题的合并具有一定的方向性。

首先,我们令 f r o n t i front_i fronti 表示 i i i 到根节点(领头羊)的距离,初始化为 0。 n u m i num_i numi 表示以 i i i 为根节点的树的大小,初始化为 1。

然后,由于战队是一条链,但是我们路径压缩之后变成了一棵树,因此在路径压缩时先要加入这样一句话:

front[x]+=front[fa[x]]

保证 f r o n t front front 更新及时,然后才能路径压缩。这里又要注意,要先计算出 g f ( f a [ x ] ) gf(fa[x]) gf(fa[x]) 并且存下之后才能更新,否则数据不够及时。

合并操作的时候,假设我们将 x x x 接到 y y y 后面,此时令 f x = g f ( x ) , f y = g f ( y ) fx=gf(x),fy=gf(y) fx=gf(x),fy=gf(y) ,要让 f a f x = n u m f y fa_{fx}=num_{fy} fafx=numfy ,因为此时此刻 x x x 不是祖先了,需要更新 f r o n t f x front_{fx} frontfx ,不过不用着急将更新下传到孩子节点,因为路径压缩会帮你做好的qwq。

此时,由于 f y fy fy 后面加入了 n u m f x num_{fx} numfx 个节点,需要更新 n u m f y + = n u m f x num_{fy}+=num{fx} numfy+=numfx ,然后清零 n u m f x num_{fx} numfx

统计答案时,不在一个集合内输出 -1 ,否则输出 ∣ f r o n t x − f r o n t y ∣ − 1 |front_{x}-front_{y}|-1 frontxfronty1 ,具体为什么请各位读者思考。

代码:

const int MAXN=30000+10;
int t,fa[MAXN],front[MAXN],num[MAXN];

int gf(int x)
{
    
    
	if(fa[x]==x) return x;
	int f=gf(fa[x]);
	front[x]+=front[fa[x]];
	return fa[x]=f;
}

int main()
{
    
    
	t=read();
	for(int i=1;i<=30000;i++) fa[i]=i,front[i]=0,num[i]=1;
	for(int i=1;i<=t;i++)
	{
    
    
		char ch;int x,y;
		cin>>ch;x=read();y=read();
		if(ch=='M')
		{
    
    
			int fx=gf(x);
			int fy=gf(y);
			if(fx!=fy)
			{
    
    
				front[fx]=num[fy];
				num[fy]+=num[fx];
				num[fx]=0;
				fa[fx]=fy;
			}
		}
		else
		{
    
    
			if(gf(x)!=gf(y)) cout<<"-1\n";
			else cout<<abs(front[x]-front[y])-1<<"\n";
		}
	}
	return 0;
}

[CEOI1999]Parity Game

这道题两种做法都可以,不过个人认为扩展域并查集更好想也更好写。

将并查集容量扩大 2 倍,如果奇偶性相同则合并 ( x , y ) , ( x + n , y + n ) (x,y),(x+n,y+n) (x,y),(x+n,y+n),否则合并 ( x , y + n ) , ( x + n , y ) (x,y+n),(x+n,y) (x,y+n),(x+n,y) 。如果两个点已经在同一个集合内,仿照上例直接判断即可。

考虑到 n n n 很大, m m m 很小,需要先离散化每一个点。(不会离散化自行百度)

代码:

const int MAXN=1e5+10;
int n,m,a[MAXN],fa[MAXN],tmp,l[MAXN],r[MAXN],q[MAXN];

int main()
{
    
    
	n=read();m=read();
	for(int i=1;i<=m;i++)
	{
    
    
		string str;
		l[i]=read();r[i]=read();cin>>str;
		q[i]=(str=="odd")?1:0;
		a[++tmp]=l[i]-1;a[++tmp]=r[i];//注意存的是l[i]-1,这里有前缀和的思想
	}
	sort(a+1,a+tmp+1);
	n=unique(a+1,a+tmp+1)-a-1;//离散化
	for(int i=0;i<=(n<<1);i++) fa[i]=i;
	for(int i=1;i<=m;i++)
	{
    
    
		int x=lower_bound(a+1,a+n+1,l[i]-1)-a;
		int y=lower_bound(a+1,a+n+1,r[i])-a;//找到离散化的点
		//非C++选手请自行打二分,C++选手不懂得查百度
		if(q[i]==1)
			if(gf(x)==gf(y)||gf(x+n)==gf(y+n)) {
    
    cout<<i-1<<"\n";return 0;}
			else hb(x,y+n),hb(x+n,y);
		else
			if(gf(x+n)==gf(y)||gf(x)==gf(y+n)) {
    
    cout<<i-1<<"\n";return 0;}
			else hb(x,y),hb(x+n,y+n);
	}
	cout<<m<<"\n";
	return 0;
}

4.总结

相信做完上述这 亿 一些例题后,各位都对并查集有了一定的了解。不过这些只是并查集的初等应用,并查集还有很多高级版本,比如可持久化并查集。这里不讲这些,太高深 且作者本人不会。并查集很多时候用于图论之中,或者是判断是否在同一个集合内。

猜你喜欢

转载自blog.csdn.net/BWzhuzehao/article/details/110008124
今日推荐