并查集例题总结

并查集例题总结

P3631 方格染色(待完成)

程序自动分析

P1955 程序自动分析

题目描述

考虑一个约束满足问题的简化版本:

假设 \(x_1,x_2,x_3,\cdots\) 代表程序中出现的变量,给定 \(n\) 个相等/不等的约束条件,请判定是否可以满足所有条件。

输入格式

输入的第一行包含一个正整数 \(t\),表示需要判定的问题个数。注意这些问题之间是相互独立的。

对于每个问题,包含若干行:

第一行包含一个正整数 \(n\),表示约束条件个数。接下来 \(n\) 行,每行包括三个整数 \(i,j,e\)。若 \(e=1\),则该约束条件为 \(x_i=x_j\)。若\(e=0\),则该约束条件为 \(x_i\neq x_j\)

解题思路

前置技能:并查集基础、离散化(提前挖坑)

对于一个并查集,相等关系好描述,只需要令相等的两个元素所在集合合并即可。不等关系呢?

对于这一道题来说,我们只需要判断是否能够全部成立,而不需要指出是第几句导致了矛盾。所以我们可以先处理相等关系,处理完所有相等之后在依次判断所有不等的变量是否在同一集合中即可。

另外,\(i,j\)的范围是\(1 \le i,j \le 10^{10}\)并查集直接开数组一定炸,所以需要离散化(下次一定)

Code

// Problem: 
//     P1955 [NOI2015] 程序自动分析
//   
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P1955
// Problem Solving Time: 2023-12-07 07:54:24
// Memory Limit: 500 MB
// Time Limit: 2000 ms

#include<bits/stdc++.h>
using namespace std;
struct ques{
	int x,y,e;
}a[10000005];//保存约束条件,因为要先处理相等,而不是按输入顺序判断 
int n;
int b[20000005],len;//离散化数组 
int p[20000005];//并查集数组 
int root(int x){//找根函数 
	if(p[x]==x)return x;
	return p[x]=root(p[x]);
}
bool cmp(ques lhs,ques rhs){
	return lhs.e>rhs.e;//lhs:lefe hand side        rhs:right hand side 
}//排序,保证在处理不等关系的时候相等关系都处理完了 
int main(){
	int t;cin>>t;
	//打完才发现循环变量重了,所以随便打了一个 
	for(int sb=1;sb<=t;sb++){
		cin>>n;
		len=0;//初始化*1
		for(int i=1;i<=n;i++){
			cin>>a[i].x>>a[i].y>>a[i].e;
			b[++len]=a[i].x;
			b[++len]=a[i].y;//备份 
		}
		sort(b+1,b+1+len);
		len=unique(b+1,b+1+len)-b-1;//去重,没有这一步其实也行 
		for(int i=1;i<=n;i++){//离散化核心部分 
			a[i].x=lower_bound(b+1,b+1+len,a[i].x)-b;
			a[i].y=lower_bound(b+1,b+1+len,a[i].y)-b;
		}
		for(int i=1;i<=len;i++)
			p[i]=i;//初始化*2 
		sort(a+1,a+1+n,cmp);//排序,让e=1的相等关系在前面 
		bool f=1;//记录能否满足 
		for(int i=1;i<=n;i++){
			if(a[i].e==1)//处理相等 
				p[root(a[i].x)]=root(a[i].y);
			else if(a[i].e==0&&root(a[i].x)==root(a[i].y)){//判断不等 
				cout<<"NO\n";
				f=0;
				break;
			}
		}
		if(f)
			cout<<"YES\n";
		//别忘了相等还要输出 
	}
	return 0;//结束 
}

银河英雄传说

P1196 银河英雄传说

题目描述

开始时第\(i\)艘战舰在第\(i\)列,一共有\(30000\)列,第一行输入一个整数\(t\),接下来有\(t\)行,每行有一条指令,每条指令有以下两种形式:

  1. M i j表示将第\(i\)号战舰所在的整个战舰队列,作为一个整体(头在前尾在后)接至第\(j\)号战舰所在的战舰队列的尾部(\(i,j\)保证不在同一列)
  2. C i j查询\(i,j\)是否在同一列,如果在,输出\(i,j\)之间隔了几艘战舰,如果不在则输出-1

解题思路

前置技能:加权并查集

打眼一看:要合并还要查找是否在同一集合,这不并查集模板吗。

仔细一看:输出间隔的战舰数(懵)

经过简单的思考之后,我的第一个想法是把并查集的路径压缩去掉,然后就能通过到根的距离来判断中间间隔的数量了。可是问题也很显然:他会超时啊。

这个时候就需要加权并查集了。路径压缩是肯定不能不做的,但压缩之后我们就丢失了这个点到根节点的距离,怎么办呢?再开一个数组把这个数据记录下来不就好了!

我们可以再开一个\(dis[]\)数组表示每个点到根节点的距离,路径压缩的时候自动把边的权值加起来就好了。问题又来了,那合并后的距离怎么维护呢?

思考这么一个问题:每次合并后合并进来的这一列的根节点到被合并的这一列的根节点距离是多少?

显而易见应该是原来这一列的战舰数。所以,我们还需要再开一个\(size[]\)数组来保存每一列一共有多少艘战舰,合并的时候只需要把加入进来的这一列的根的父亲设置为被加入这一列的根,然后距离设置为这一列战舰的个数,其他的距离在路径压缩的时候自己就会加上了。

Code

// Problem: 
//     P1196 [NOI2002] 银河英雄传说
//   
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P1196
// Problem Solving Time: 2023-12-07 07:53:50
// Memory Limit: 128 MB
// Time Limit: 1000 ms

#include<bits/stdc++.h>
using namespace std;
int t;//操作数量 
int p[30004];//并查集数组 
int dis[30004];//记录每个点到他的父亲的边权 
int size[30004];//记录每一列战舰的数量 
int root(int x){//带权值版找根函数 
    if(x==p[x]) return x;
    int t=root(p[x]);
	//记录它的根,顺便把他前面的节点全部路径压缩
	//确保p[x]直接是根或者再根下面那一层,从而确保dis[p[x]]记录的就是x的父亲到根的距离 
    dis[x]+=dis[p[x]];//把边的权值加起来 
    return p[x]=t;//路径压缩
}
int main(){
	cin>>t;
	for(int i=1;i<=30000;i++)
		p[i]=i,size[i]=1;//初始化,别忘了每一列初始的大小都是一,范围题目里说了 
	for(int i=1;i<=t;i++){
		char op;int x,y;cin>>op>>x>>y;
		if(op=='M'){//合并 
			int rx=root(x),ry=root(y);
			dis[rx]=size[ry];//更新权值 
			size[ry]+=size[rx];//更新大小
			//更新权值和大小的两行顺序不能颠倒,不然直接WA 
			p[rx]=ry;//合并 
		}else{//查找 
			if(root(x)!=root(y))
				cout<<-1<<endl;
			else
				cout<<abs(dis[x]-dis[y])-(x!=y)<<endl;
				//这行代码效果同:
				//if(x==y)cout<<"0";
				//else cout<<abs(dis[x]-dis[y])-1
				//至于为什么这么写自己画几个图研究一下 
		}	 
	}
	return 0;//结束 
}

Parity Game

P5937 Parity Game

题目描述

Alice 和 Bob 在玩一个游戏:他写一个由 \(0\)\(1\) 组成的序列。Alice 选其中的一段(比如第 \(3\) 位到第 \(5\) 位),问他这段里面有奇数个\(1\)还是偶数个\(1\)(含两端点)。Bob 回答你的问题,然后 Alice 继续问。Alice 要检查 Bob 的答案,指出在 Bob 的第几个回答第一次与前面矛盾。如果所有回答都合理,则输出所有回答的个数。

解题思路

前置技能:种类并查集(又称扩展域并查集)(对于扩展域并查集不熟悉的可以先去看下一道题)、离散化

设一个前缀和数组\(s[]\),表示\(i\)的个数(用来辅助思考,并不代表代码中要使用前缀和)
\(i(1 \le i \le n)\)在并查集所在的集合中的元素与\(s[i]\)的奇偶性相同,\(i+n\)在并查集所在的集合中的元素与\(s[i]\)的奇偶性不同

如果\(x\)~\(y\)之间为奇数, 则\(s[x-1]\)\(s[y]\)的奇偶性不同 ,按照上文所设, 我们应该把\(x-1\)\(y+n\)合并,把\(x-1+n\)\(y\)合并
如果\(x\)~\(y\)之间为偶数, 则\(s[x-1]\)\(s[y]\)的奇偶性相同 ,按照上文所设, 我们应该把\(x-1\)\(y\)合并, 把\(x-1+n\)\(y+n\)合并

理解了这两句话,种类并查集(扩展域并查集)就基本理解了。如果无法理解可以尝试先不管代码具体实现,先想通这层逻辑。

数据规模达到了\(10^9\),所以肯定还需要离散化

Code

// Problem: 
//     P5937 [CEOI1999] Parity Game
//   
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P5937
// Problem Solving Time: 2023-12-07 07:53:28
// Memory Limit: 128 MB
// Time Limit: 1000 ms

#include<bits/stdc++.h>
using namespace std;
int n,m;//rt 
int a[5003][3];//将输入的问题保存下来,因为要先经过离散化 
int p[50004],len;
//懒得开两个数组了,所以你会发现,在程序的前半段p是离散化数组
//后半段就变成了并查集数组 
string s;
int root(int x){//找根函数 
	if(p[x]==x)return x;
	return p[x]=root(p[x]);
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		cin>>a[i][0]>>a[i][1]>>s;
		a[i][0]--;//提前-1,因为要进行离散化 
		a[i][2]=(s=="odd");
		p[++len]=a[i][0];
		p[++len]=a[i][1];//离散化备份 
	}
	sort(p+1,p+1+len);//排序
	len=unique(p+1,p+1+len)-p-1;//去重 
	for(int i=1;i<=m;i++){//离散化处理 
		a[i][0]=lower_bound(p+1,p+1+len,a[i][0])-p;
		a[i][1]=lower_bound(p+1,p+1+len,a[i][1])-p;
	}
	for(int i=1;i<=2*m;i++)//因为扩展为了两个域,所以这里写2*m(别写成n了) 
		p[i]=i;//p化身为并查集 
	for(int i=1;i<=m;i++){
		if(a[i][2]){//如果是奇数
			if(root(a[i][0])==root(a[i][1])){
				//如果满足这个式子就表示根据前面的条件,这两个端点之间应该有偶数个(奇偶性相同)
				//不可以写成!root(a[i][0])!=root(a[i][1]),这表示他们两个没有关系或者矛盾 
				cout<<i-1;//与前面的条件相悖,直接终止 
				return 0;
			}else{//符合条件或者之前没有关于两端点的限制 
				p[root(a[i][0])]=p[root(a[i][1]+len)];
				p[root(a[i][1])]=p[root(a[i][0]+len)];
			}
		}else{//偶数
			if(root(a[i][0])==root(a[i][1]+len)){
				//同上 
				cout<<i-1;
				return 0;
			}else{//同上 
				p[root(a[i][0])]=p[root(a[i][1])];
				p[root(a[i][1]+len)]=p[root(a[i][0]+len)];
			}
		}
	}
	cout<<m;//别忘了最后如果都满足要输出问题数 
	return 0;//结束 
}

团伙

P1892 团伙

题目描述

现在有 \(n\) 个人,他们之间有两种关系:朋友和敌人。我们知道:

  • 一个人的朋友的朋友是朋友

  • 一个人的敌人的敌人是朋友

现在要对这些人进行组团。两个人在一个团体内当且仅当这两个人是朋友。请求出这些人中最多可能有的团体数。

第一行输入一个整数 \(n\) 代表人数。

第二行输入一个整数 \(m\) 表示接下来要列出 \(m\) 个关系。

接下来 \(m\) 行,每行一个字符 \(op\) 和两个整数 \(p,q\),分别代表关系(朋友或敌人),有关系的两个人之中的第一个人和第二个人。其中 \(op\) 有两种可能:

  • 如果 \(op\)F,则表明 \(p\)\(q\) 是朋友。
  • 如果 \(op\)E,则表明 \(p\)\(q\) 是敌人。

解题思路

前置知识:扩展域并查集(种类并查集)

可以说扩展域并查集的模板了吧~

对于这个题,我们定义如果这个集合中包含\(i(1 \le i \le n)\),那么这个集合中其他元素所表示的就是\(i\)的朋友(或称此集合为\(i\)的朋友域)。

集合中的\(i+n\)表示这个集合中其他元素所表示的就是\(i\)的敌人(或称此集合为\(i\)的敌人域)。

最后的答案就是总共集合的个数

Code

// Problem: 
//     P1892 [BOI2003] 团伙
//   
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P1892
// Problem Solving Time: 2023-12-07 07:53:05
// Memory Limit: 125 MB
// Time Limit: 1000 ms

#include<bits/stdc++.h>
using namespace std;
int n,m;//输入 
int p[2003];//并查集数组 
int root(int x){//找根函数 
	if(p[x]==x)return x;
	return p[x]=root(p[x]);//路径压缩别忘了 
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n*2;i++)
		p[i]=i;//初始化不能少 
	for(int i=1;i<=m;i++){
		int x,y;char op;cin>>op>>x>>y;
		if(op=='F'){//他们是朋友 
			p[root(x)]=root(y);//朋友的朋友是朋友 
//			p[root(x+n)]=root(y+n);千万不能加!!!朋友的敌人不一定是敌人 
		}else{//他们是敌人 
			p[root(x+n)]=root(y);//敌人的敌人是朋友 
			p[root(y+n)]=root(x);//敌人的敌人是朋友 
			//一定要写成这样,如果写成 
			//p[root(y)]=root(x+n);
			//p[root(x)]=root(y+n);
			//最后求和的时候就要计算1~2*n,因为这可能导致一个集合的根大于n 
		}
	}
	int cnt=0;//计数器 
	for(int i=1;i<=n;i++)//统计,注意事项如上文所述 
		if(p[i]==i)cnt++;
	cout<<cnt;//输出 
	return 0;//结束 
}

食物链

P2024 食物链

题目描述

动物王国中有三类动物 \(A,B,C\),这三类动物的食物链构成了有趣的环形。\(A\)\(B\)\(B\)\(C\)\(C\)\(A\)

现有 \(N\) 个动物,以 \(1 \sim N\) 编号。每个动物都是 \(A,B,C\) 中的一种,但是我们并不知道它到底是哪一种。

有人用两种说法对这 \(N\) 个动物所构成的食物链关系进行描述:

  • 第一种说法是 1 X Y,表示 \(X\)\(Y\) 是同类。
  • 第二种说法是2 X Y,表示 \(X\)\(Y\)

此人对 \(N\) 个动物,用上述两种说法,一句接一句地说出 \(K\) 句话,这 \(K\) 句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。

  • 当前的话与前面的某些真的话冲突,就是假话;
  • 当前的话中 \(X\)\(Y\)\(N\) 大,就是假话;
  • 当前的话表示 \(X\)\(X\),就是假话。

你的任务是根据给定的 \(N\)\(K\) 句话,输出假话的总数。

第一行两个整数,\(N,K\),表示有 \(N\) 个动物,\(K\) 句话。

第二行开始每行一句话(按照题目要求,见样例)

解题思路

前置技能:扩展域并查集

这个题和上一道题很像,只不过从两个域又加成了三个域,即同类域,天敌域,猎物域。

那么合并的条件就变成了:

  • 同类的猎物是猎物,同类的同类是同类,同类的天敌是天敌

  • 天敌的猎物是同类,天敌的同类是天敌,天敌的天敌是猎物

  • 猎物的猎物是天敌,猎物的天敌是同类,猎物的同类是猎物

看起来多,实际上这三句话是一个意思 (绝对不是在水字数)

就是模板的思路,细节就看注释吧

Code

// Problem: 
//     P2024 [NOI2001] 食物链
//   
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P2024
// Problem Solving Time: 2023-12-07 07:52:36
// Memory Limit: 125 MB
// Time Limit: 1000 ms

#include<bits/stdc++.h>
using namespace std;
int n,k,ans;
int p[150004];//并查集数组 
int root(int x){//找根函数 
	if(p[x]==x)return x;
	return p[x]=root(p[x]);
}
int main(){
	cin>>n>>k;
	//点的三个域在分别同一个集合表示有关系,不在同一个集合表示不知道他们的关系 
	//1~n同类域  i+n猎物域  i+2n天敌域 
	for(int i=1;i<=n*3;i++)
		p[i]=i;//初始化 
	for(int i=1;i<=k;i++){
		int op,x,y;cin>>op>>x>>y;
		if(x>n||y>n){//特判不能忘 
			ans++;
			continue;
		}
		if(op==1){//xy同类 
			if(root(x)==root(y+n)||root(x)==root(y+2*n)){
				//如果根据前面的限定,x的同类域是y的猎物域(y eat x)或者x的同类域是y的天敌域(x eat y)
				//也可以写成........(省略好几种等效的写法,因为每次建立关系都是在同类、猎物、天敌域建立三条关系,所以知一就可以推二) 
				ans++;
				continue;//别直接结束了 
			}else{//如果满足或者之前没有这两种动物之间的关系 
				p[root(x)]=root(y);//同类的同类是同类 
				p[root(x+n)]=root(y+n);//同类的猎物是猎物 
				p[root(x+2*n)]=root(y+2*n);//同类的天敌是天敌 
			}
		}
		else{//x eat y 
			if(root(x)==root(y)||root(x+2*n)==root(y)){
				//根据上文自己对照着想吧 
				ans++;
				continue;
			}else{//如果满足或者之前没有这两种动物之间的关系 
				p[root(x)]=root(y+2*n);//x的同类是y的天敌 
				p[root(x+n)]=root(y);//x的猎物是y的同类 
				p[root(x+2*n)]=root(y+n);//x的天敌是y的猎物 
			}
		}
	}
	cout<<ans;//输出 
	return 0;//结束 
}

关押罪犯

P1525 关押罪犯

题目描述

现有两座监狱,一共关押着 \(N\) 名罪犯,编号分别为 \(1\sim N\)。我们用“怨气值”(一个正整数值)来表示某两名罪犯之间的仇恨程度。如果两名怨气值为 \(c\) 的罪犯被关押在同一监狱,他们俩会造成影响力为 \(c\) 的冲突事件。

每年年末,警察局会将本年内监狱中的所有冲突事件按影响力从大到小排成一个列表。市长只会注意到影响力最大的事件。

在详细考察了 \(N\) 名罪犯间的矛盾关系后,警察局长觉得压力巨大。他准备将罪犯们在两座监狱内重新分配,以求产生的冲突事件影响力都较小,从而保住自己的乌纱帽。假设只要处于同一监狱内的某两个罪犯间有仇恨,那么他们一定会在每年的某个时候发生摩擦。

那么,应如何分配罪犯,才能使 Z 市长看到的那个冲突事件的影响力最小?这个最小值是多少?

第一行为两个正整数 \(N,M\),分别表示罪犯的数目以及存在仇恨的罪犯对数。接下来的 \(M\) 行每行为三个正整数 \(a_j,b_j,c_j\),表示 \(a_j\) 号和 \(b_j\) 号罪犯之间存在仇恨,其怨气值为 \(c_j\)。数据保证 \(1<a_j\leq b_j\leq N, 0 < c_j\leq 10^9\),且每对罪犯组合只出现一次。

共一行,为 Z 市长看到的那个冲突事件的影响力。如果本年内监狱中未发生任何冲突事件,请输出 0

解题思路

解题的关键在于要先解决影响力大的囚犯事件,所以要进行排序,每一组仇恨关系都相当于一种关系,因为只有两个监狱,所以处理的时候就相当于"敌人的敌人是朋友",所以也可以用扩展域并查集来做。

\(i(1 \le i \le n)\)定义为\(i\)的同类域,\(i+n\)定义为\(i\)的不同域

Code

// Problem: 
//     P1525 [NOIP2010 提高组] 关押罪犯
//   
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P1525
// Problem Solving Time: 2023-12-07 07:41:25
// Memory Limit: 125 MB
// Time Limit: 1000 ms

#include<bits/stdc++.h>
using namespace std;
struct abcd{
	int x,y,c;
}a[100005];//需要按照怨气值排序
int n,m;
int p[200005];//并查集数组
bool cmp(abcd lhs,abcd rhs){//比较函数,先处理影响力大的
	return lhs.c>rhs.c;
}
int root(int x){//找根函数
	if(p[x]==x)return x;
	return p[x]=root(p[x]);//路径压缩
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n*2;i++)//初始化
		p[i]=i;
	for(int i=1;i<=m;i++)
		cin>>a[i].x>>a[i].y>>a[i].c;
	sort(a+1,a+1+m,cmp);//排序
	for(int i=1;i<=m;i++){
		if(root(a[i].x)==root(a[i].y)){
			cout<<a[i].c;//如果与之前的冲突了,就说明为了保证影响力更大的事件不发生,这个事件必须发生
			return 0;
		}else{//不冲突,把他俩安排在不同监狱
			p[root(a[i].x)]=root(a[i].y+n);//与x同监狱的是和y不同监狱的
			p[root(a[i].y)]=root(a[i].x+n);//与y同监狱的是和x不同监狱的
		}
	}
	cout<<0;//所有事件都处理完成,都不冲突记得输出0
	return 0;
}

白雪皑皑

P2391 白雪皑皑

题目描述

现在有 \(n\) 片雪花排成一列。要进行 \(m\) 次染色操作,第 \(i\) 次染色操作中,把第 \(((i\times p+q)\bmod n)+1\) 片雪花和第 \(((i\times q+p)\bmod n)+1\) 片雪花之间的雪花(包括端点)染成颜色 \(i\)。其中 \(p,q\) 是给定的两个正整数。他想知道最后 \(n\) 片雪花被染成了什么颜色。没有被染色输出 \(0\)

解题思路

终于来了一道不是扩展域并查集和加权并查集的!

这道题重要的是在思路。首先思考暴力应该怎么写?显然,我们应该枚举\(m\)次染色操作,对于每次操作,我们最坏要进行n次查询,所以暴力的复杂度是\(O(n \times m)\)

哪里还能优化呢?首先,每个点最后的颜色只需要知道他最后一次被染色是在第几次。所以,我们可以倒序进行染色操作,在遇到已经染过色的的点时直接跳到下一个可操作的点,这样倒序执行完\(m\)次操作之后一共就对\(n\)个点进行了操作,复杂度就骤降到了\(O(n + m)\)

至于要如何跳到下一个可执行点呢?用并查集记录下来不就好了!

Code

// Problem: 
//     P2391 白雪皑皑
//   
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P2391
// Problem Solving Time: 2023-12-07 08:16:29
// Memory Limit: 125 MB
// Time Limit: 1000 ms

#include<bits/stdc++.h>
using namespace std;
int n,m,p,q;
int c[1000006];//color 记录颜色
int f[1000006];//并查集数组
int root(int x){//找根函数
	if(f[x]==x)return x;
	return f[x]=root(f[x]);//路径压缩
}
int main(){
	cin>>n>>m>>p>>q;
	for(int i=1;i<=n+1;i++)
		f[i]=i;//初始化
	for(int i=m;i>0;i--){//倒序操作,正序操作染色之后会被覆盖,就不能这么优化了
		int sp=min((i*q+p)%n+1,(i*p+q)%n+1),ep=max((i*q+p)%n+1,(i*p+q)%n+1);//sp:start piont  ep:end point
		//预处理好染色的起点和终点,因为不确定哪个是起点哪个是终点所以用min和max
		for(int j=sp;j<=ep;j++){//染色
			j=root(j);//直接跳到下一个空点
			if(j<=ep){//记得判断,不然可能一下跳出去了
				f[j]=root(ep+1);//染色之后保证从起点到终点都不可操作了,所以下一个可执行点设为ep加一
				c[j]=i;//记录颜色
			}
		}
	}
	for(int i=1;i<=n;i++)
		cout<<c[i]<<endl;//输出
	return 0;//结束
}