并查集+经典例题

并查集原来以前看过,当时看的那篇文章编了一个江湖的关系,令我印象深刻,可惜我找不到了。
并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。常常在使用中以森林来表示。

基础

模板题

#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+10;
int n,m;
int z,x,y;
int f[maxn];
void init(){
	for(int i=1;i<=n;i++)
	f[i]=i;
}
int getFriend(int x){
	if(f[x]==x)
	return x;
	return  f[x]=getFriend(f[x]);
}
void merge(int x,int y){
	int t1=getFriend(x);
	int t2=getFriend(y);
	if(t1!=t2){
		f[t2]=t1; //靠左原则
	}
}
int main(){
	scanf("%d%d",&n,&m);
	init();
	for(int i=1;i<=m;i++){
		cin>>z>>x>>y;
		if(z==1){
			merge(x,y);
		}
		else{
			if(getFriend(x)==getFriend(y)){
				cout<<"Y"<<endl;
			}
			else
			cout<<"N"<<endl;
		}
	}
	return 0;
}

优化

靠左原则或者靠右原则还可以进行优化,所以我们改变合并两个朋友圈的方式,变成“比较朋友圈高度原则”。(按秩合并)
将高度较小的朋友圈合并到高度较大的朋友圈里,合并完后调整高度即可。

#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+10;
int n,m;
int z,x,y;
int f[maxn];
int h[maxn];
void init(){
	for(int i=1;i<=n;i++)
	f[i]=i;
}
int getFriend(int x){
	if(f[x]==x)
	return x;
	return  f[x]=getFriend(f[x]);
}
void merge(int x,int y){
	int t1=getFriend(x);
	int t2=getFriend(y);
	if(t1!=t2){
		if(h[t2]>h[t1])
		f[t2]=t1;
		else{
			f[t1]=t2;
			if(h[t2]==h[t1])
			h[t1]++;
		}
	}
}
int main(){
	scanf("%d%d",&n,&m);
	init();
	for(int i=1;i<=m;i++){
		cin>>z>>x>>y;
		if(z==1){
			merge(x,y);
		}
		else{
			if(getFriend(x)==getFriend(y)){
				cout<<"Y"<<endl;
			}
			else
			cout<<"N"<<endl;
		}
	}
	return 0;
}

种类并查集

(1)并查集可以维护连通性、传递性,通俗的说朋友的朋友是朋友,
然而我们还需要维护一些对立的关系,比如敌人的敌人是朋友,所以正常的并查集很难满足我们的需求,所以种类并查集诞生。
(2)常见的种类并查集是将原来的并查集扩大一倍规模,并划分为两个种类。
在同个种类的并查集合并,代表他们是朋友,在不同种的并查集,他们就是敌人。
(3)按照并查集的传递性,我们就能具体知道两个是元素是朋友还是敌人。
(4)至于某个元素到底属于两个种类中的哪一个,由于我们不清楚,因此两个种类我们都试试。

A Bug’s Life

传送门

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=2e3+10;
bool ok;
int f[maxn*2],t,n,m;
void init(){
	for(int i=1;i<=2*n;i++){
		f[i]=i;
	}
}
int find(int x){
	if(f[x]==x)
	return x;
	return x=find(f[x]);
}
void merge(int x,int y){
	int t1=find(x);
	int t2=find(y);
	if(t1!=t2){
		f[t2]=t1;
	}
}
int main(){
	cin>>t;
	for(int i=1;i<=t;i++){
		cin>>n>>m;
		init();
		ok=false;
		for(int j=1;j<=m;j++){
			int x,y;
			cin>>x>>y;
			if(find(x)!=find(y)){
				merge(x+n,y);
				merge(x,y+n);
			}
			else{
				ok=true;
			}
		}
		if(ok)
		printf("Scenario #%d:\nSuspicious bugs found!\n\n",i);
		else
		printf("Scenario #%d:\nNo suspicious bugs found!\n\n",i);
	}
}

再上一种

#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=2e3+10;
int t,n,m,f[maxn],b[maxn],x,y;
bool ok;
void init(){
	memset(b,0,sizeof(b));
	for(int i=1;i<=maxn;i++)
	f[i]=i;
}
int find(int x){
	if(f[x]==x)
	return x;
	return x=find(f[x]);
}
void merge(int x,int y){
	int t1=find(x);
	int t2=find(y);
	if(t1!=t2){
		f[t1]=t2;
	}
}
int main(){
	scanf("%d",&t);
	for(int i=1;i<=t;i++){
		scanf("%d%d",&n,&m);
		ok=false;
		init();
		while(m--){
			scanf("%d%d",&x,&y);
			if(find(x)!=find(y)){
			if(!b[x])b[x]=y;
			else merge(b[x],y);
			if(!b[y])b[y]=x;
			else merge(b[y],x);				
			}
			else ok=true;
		}
	if(ok)printf("Scenario #%d:\nSuspicious bugs found!\n\n",i);
	else printf("Scenario #%d:\nNo suspicious bugs found!\n\n",i);
	}
	return 0;
}

关押罪犯

传送门

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e5+10;
struct node{int x,y;ll c;}s[maxn];
int n,m;
int b[maxn];
int f[maxn];
int h[maxn];
bool cmp(const node&a,const node&b){
	return a.c>b.c;
}
void init(){
	for(int i=1;i<=n;i++)
	f[i]=i;
	return;
}
int find(int x){
	if(f[x]==x)
	return x;
	return x=find(f[x]);
}
void merge(int a,int b){
	int t1=find(a);
	int t2=find(b);
	if(h[t1]>h[t2])
	f[t2]=t1;
	else{
		f[t1]=t2;
		if(h[t1]==h[t2])
		h[t1]++;
	}
}
int main(){
	scanf("%d%d",&n,&m);
	init();
	for(int i=1;i<=m;i++)
	scanf("%d%d%lld",&s[i].x,&s[i].y,&s[i].c);
	sort(s+1,s+1+m,cmp);
	for(int i=1;i<=m+1;i++){//m+1是为了输出0的情况
		if(find(s[i].x)==find(s[i].y)){
			printf("%lld\n",s[i].c);
			break;
		}
		if(!b[s[i].x])b[s[i].x]=s[i].y;
		else merge(b[s[i].x],s[i].y);
		if(!b[s[i].y])b[s[i].y]=s[i].x;
		else merge(b[s[i].y],s[i].x);
	} 
	return 0;
}

或者另一种写法与食物链类似的一种写法

#include<bits/stdc++.h>
using namespace  std;
typedef long long ll;
const int maxn=1e5+10;
int n,m;
int h[maxn],f[maxn];
struct node{int x,y;ll c;}s[maxn];
bool cmp(const node&a,const node&b){
	return a.c>b.c;
}
void init(){
	for(int i=1;i<=n*2;i++)
	f[i]=i;
}
int find(int x){
	if(f[x]==x)
	return x;
	return x=find(f[x]);
}
void merge(int a,int b){
	int t1=find(a);
	int t2=find(b);
	if(h[t2]>h[t1])
	f[t2]=t1;
	else{
	f[t1]=t2;
	if(h[t1]==h[t2])
	h[t1]++;
	}
}
int main(){
	scanf("%d%d",&n,&m);
	init();
	for(int i=1;i<=m;i++){
		scanf("%d%d%lld",&s[i].x,&s[i].y,&s[i].c);
	}
	sort(s+1,s+1+m,cmp);
	for(int i=1;i<=m+1;i++){
		if(find(s[i].x)==find(s[i].y))
		{
			printf("%lld\n",s[i].c);
			break;
		}
		else{
			merge(f[s[i].x+n],f[s[i].y]);
			merge(f[s[i].x],f[s[i].y+n]);
		}
	}
	return 0;
}

注意事项:种类并查集求的并不是具体种类,而是关系
3倍并查集。
x元素:[1,n]是同类
[n+1,2n]是猎物
[2n+1,3n]是天敌
感觉关系是循环的

食物链

传送门

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
int s[maxn*3],f[maxn*3],h[maxn*3];
int n,k;
int sum;
void init(){
	for(int i=1;i<=n*3;i++){
		f[i]=i;
	}
	return;
}
int find(int x){
	if(f[x]==x)
	return x;
	return x=find(f[x]);
}
void merge(int a,int b){
	int t1=find(a);
	int t2=find(b);
	if(h[t1]>h[t2])
	f[t2]=t1;
	else{
		f[t1]=t2;
		if(h[t1]==h[t2])
		h[t1]++;
	} 
}
int main(){
	scanf("%d%d",&n,&k);
	init();
	for(int i=1;i<=k;i++){
		int opt,x,y;
		scanf("%d%d%d",&opt,&x,&y);
		if(x>n||y>n){
			sum++;
			continue;
		}
		if(opt==1){//x与y同类 
			if(x==y)
			continue;
			if(find(x+n)==find(y)||find(x+2*n)==find(y))
			sum++;
			else{
			merge(x,y);
			merge(x+n,y+n);
			merge(x+2*n,y+2*n);				
			}
		}
		else{//x吃y 
			if(x==y||find(x)==find(y)||find(x+2*n)==find(y))
			sum++;
			else{
				merge(x+n,y);
				merge(x+n*2,y+n);
				merge(x,y+2*n);
			}
		}
	}
	cout<<sum<<endl;
	return 0;
} 

带权并查集

带权并查集即是结点存有权值信息的并查集;带权并查集每个元素的权通常描述其与并查集中祖先的关系,这种关系如何合并,路径压缩时就如何压缩带权并查集可以推算集合内点的关系,而一般并查集只能判断属于某个集合。

要考虑压缩路径时的维护关系,我们压缩路径时知道A和B的关系,知道A和A根结点的关系,需要推导B与A根结点的关系

权值具体是什么要根据题意,一般都是两个节点之间的某一种相对的关系
带权并查集的权值,要随着压缩路径的过程进行更新

A Bug’s Life

传送门
学了一天的带权并查集,终于稍微懂点了。
这道题比关押罪犯简单,与食物链类似但还是简单,因为这道题就两个关系,一个就是同性、一个就是异性,分别用0和1表示。
(1)首先是合并考虑压缩路径时的关系维护,我们压缩路径时已知B和A的关系,以及A和A根节点的关系,需要推导出B和A根节点的关系
在这里插入图片描述

A与根结点的关系B与A的关系B与A根结点的关系
000
011
101
110
int find(int x){
	if(x==f[x])
	return x;
	int t=find(f[x]);//找到父亲节点的根结点
	v[x]=(v[x]+v[f[x]])%2;//更新权值
	f[x]=t;//更新x的父亲节点
	return f[x];
}

(2) 然后我们考虑关系的查找,我们以及知道A和B在同一集合,即代表他们根节点相同,我们要确定两者之间的关系,根据这题,如果A与B是1的话,符合题意,A与B是0的话,就是可疑缺陷发现。
在这里插入图片描述

A与根结点的关系B与根结点的关系A与B的关系
000
011
101
110
	if(t1==t2){
		if((v[a]+v[b])%2==0)
		return true;
		else return false;
	}

(3) 最后我们考虑合并两个节点时关系的维护,我们已经知a和其根节点的关系,以及b和其根节点的关系,当我们把b集合合并到a集合时,我们需要考虑b根节点和a根节点存在的关系
在这里插入图片描述
也是根据表格的关系得出代码
最后注意一下,发现有缺陷后,不能马上break,因为数据还没有输入完

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=2e3+10;
int n,m,t;
bool ok;
int f[maxn],v[maxn];
void init(){
	for(int i=1;i<=maxn;i++){
		f[i]=i;
		v[i]=0;
	}
}
int find(int x){
	if(x==f[x])
	return x;
	int t=find(f[x]);//root
	v[x]=(v[x]+v[f[x]])%2;
	f[x]=t;
	return f[x];
}
bool merge(int a,int b){
	int t1=find(a);
	int t2=find(b);
	if(t1==t2){
		if((v[a]+v[b])%2==0)
		return true;
		else return false;
	}
	else{
		f[t1]=t2;
		v[t1]=(1+v[a]+v[b])%2;
	}
	return false;
}
int main(){
	cin>>t;
	for(int i=1;i<=t;i++){
		ok=false;
		scanf("%d%d",&n,&m);
		init();
		for(int j=1;j<=m;j++){
			int x,y;
			scanf("%d%d",&x,&y);
			if(merge(x,y)){ 
				ok=true;//不能break
			}
		}
		if(ok){
			printf("Scenario #%d:\nSuspicious bugs found!\n\n",i);
		}
		else{
			printf("Scenario #%d:\nNo suspicious bugs found!\n\n",i);
		}
	}
	return 0;
}

食物链

传送门
让我们再谈食物链这道题

我们需要创建pre数组和rela数组,pre数组判断集合之间的关系,rela判断集合内部的关系。这题我们建立三种关系:X与Y是同类、X是Y的猎物(Y吃X)、X是Y的天敌(X吃Y),在rela数组分别用0,1,2表示。
在这里插入图片描述

A与其根结点的关系B与A的关系B与A的根结点之间的关系
000
011
022
101
112
120
202
210
221

所以说可以得到rela[b]=(rela[b]+rela[a])%3的压缩路径关系

int find(int x){ 
	if(x==pre[x])
	return x;
	else{
		int temp=pre[x];
		pre[x]=find(pre[x]);
		rela[x]=(rela[x]+rela[temp])%3;
	}
	return pre[x];
}

然后考虑关系的查找,A与B根结点相同,他们一定是在同一集合,我们要确定A、B两者的关系。
在这里插入图片描述

A与根结点B与根结点A与B的关系
000
012
021
101
110
122
202
211
220

rela[a->b]=(rela[a]-rela[b]+3)%3;

if(find(a)==find(b)){
	relation=(rela[a]-rela[b]+3)%3;
	return relation==r;
}

最后考虑合并两个节点关系的维护
在这里插入图片描述
rela[pre[B]->pre[A]]=(rela[A]+rela[B->A]-rela[pre[B]]+3)%3

void merge(int x,int y,int r){
	int t1=find(x);
	int t2=find(y);
	if(t1!=t2){
		pre[t1]=pre[t2];
		rela[t1]=(rela[y]-rela[x]+r+3)%3;
	}
}

P1196 [NOI2002]银河英雄传说

传送门
只可能一次移动整个队列,并且是**把两个队列首尾相接合并成一个队列,不会出现把一个队列分开的情况,**因此,我们必须要找到一个可以一次操作合并两个队列的方法。
再来看下C指令:判断飞船i和飞船j是否在同一列,若在,则输出它们中间隔了多少艘飞船。我们先只看判断是否在同一列,由于每列一开始都只有一艘飞船,之后开始合并,结合刚刚分析过的M指令,很容易就想到要用并查集来实现。
两艘飞船之间的飞船数量,其实就是艘飞船之间的距离,那么,这就转换为了一个求距离的问题。看见多次求两个点的距离的问题,便想到用前缀和来实现
v[]是权值即当前点到船头的距离,d[i]表示以i为首的船的数量
以t1为首的船头与t2为首的船头合并,一开始的v[t1]等于0,所以要用d[]记录

#include<bits/stdc++.h>
using namespace std;
const int maxn=3e4+10;
int t,f[maxn],v[maxn],d[maxn];
void init(){
	for(int i=1;i<=maxn;i++){
		f[i]=i;
		d[i]=1;
	}
}
int find(int x){
	if(x==f[x])
	return x;
	int t=find(f[x]);
	v[x]=v[x]+v[f[x]];
	f[x]=t;
	return f[x];
}
void merge(int a,int b){
	int t1=find(a);
	int t2=find(b);
	f[t1]=t2; //将以t1为船首的队列指到以t2为船首的队列
	v[t1]=d[t2];t1到t2的距离那就是t2原来的船数
	d[t2]+=d[t1];//t2现在的船数等于t2的加t1的
}
int main(){
	init();
	scanf("%d",&t);
	while(t--){
		char c;int x,y;
		cin>>c>>x>>y;
		if(c=='M'){
			merge(x,y);
		}
		else{
			if(find(x)!=find(y))
			printf("-1\n");
			else{
				printf("%d\n",abs(v[x]-v[y])-1);
			}
		}
	}
	return 0;
} 

P2342 叠积木

传送门

#include<bits/stdc++.h>
using namespace std;
const int maxn=3e4+10;
int n;
int f[maxn],v[maxn],d[maxn];
void init(){
	for(int i=1;i<=maxn;i++)
	{
		f[i]=i;
		d[i]=1;
	}
}
int find(int x){
	if(x==f[x])
	return x;
	int t=find(f[x]);
	v[x]=v[x]+v[f[x]];
	return f[x]=t;
}
void merge(int a,int b){
	int t1=find(a);
	int t2=find(b);
	f[t2]=t1;
	v[t2]=d[t1];
	d[t1]+=d[t2];
}
int  main(){
	scanf("%d",&n);
	init();
	while(n--){
		char opt;int x,y;
		cin>>opt;
		if(opt=='M'){
			scanf("%d%d",&x,&y);
			merge(x,y);
		}
		else{
			scanf("%d",&x);
			printf("%d\n",d[find(x)]-v[x]-1);
		}
	}
	return 0;
}
  • 4
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值