ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

并查集

2022-07-22 08:03:43  阅读:149  来源: 互联网

标签:ch return int 查集 fa maxn find


一般对于连通性问题,并查集是非常好用的
不仅仅可以维护出是否连通,还可以顺便维护出当前连通块的一些信息,比如直径
有时候并查集的祖先节点不做区分,即合并是认定父子关系是随意的,但是是遇到构建 \(kruscal\) 重构树等情形就需要做出区分,甚至在并查集树上做一些事情,所以一般情况下不要乱写
一般情况下路径压缩就可以满足复杂度需求,然而路径压缩不具有可撤销性或造成树形结构紊乱,所以遇到线段树分治等情形必须使用按秩合并


经典例题

P1197 [JSOI2008]星球大战

这是一个经典套路:时光倒流
对于拆分连通块的题,往往可以通过时间倒流的方式转化为连通块的合并

代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=4e5+5;
int n,fa[maxn],tot,x,y,m,k,a[maxn],ans[maxn],cnt;
bool vis[maxn];
vector<int>edge[maxn];
int read(){
	int x=0,f=1;
	char ch=getchar();
	while(!isdigit(ch)){
		if(ch=='-')f=-1;
		ch=getchar();
	}
	while(isdigit(ch)){
		x=x*10+ch-48;
		ch=getchar();
	}
	return x*f;
}
int find(int x){
	return fa[x]==x?x:fa[x]=find(fa[x]);
}
void unionn(int x,int y){
	x=find(x);
	y=find(y);
	if(x!=y)fa[y]=x,tot--;
	//,cout<<x<<" "<<y<<endl;
	return ;
}
int main(){
	n=read();
	m=read();
	for(int i=1;i<=m;i++){
		x=read()+1;
		y=read()+1;
		edge[x].push_back(y);
		edge[y].push_back(x);
	}
	k=read();
	for(int i=1;i<=k;i++){
		a[i]=read()+1;
		vis[a[i]]=true;
	}
	tot=n-k;
//	cout<<tot<<" ";
	for(int i=1;i<=n;i++){
		fa[i]=i;
	}
	for(int i=1;i<=n;i++){
//		fa[i]=i;
//		tot++;
		if(!vis[i]){
			for(int j=0;j<edge[i].size();j++){
				if(!vis[edge[i][j]]){
					unionn(i,edge[i][j]);
				}
			}
		}
	}
	ans[++cnt]=tot;
//	cout<<tot<<endl;
	for(int i=k;i>=1;i--){
		tot++;
		for(int j=0;j<edge[a[i]].size();j++){
			if(!vis[edge[a[i]][j]]){
				unionn(a[i],edge[a[i]][j]);
			}
		}
		vis[a[i]]=false;
		ans[++cnt]=tot;
//		printf("%d\n",tot);
	}
	for(int i=k+1;i>=1;i--)printf("%d\n",ans[i]);
	return 0;
}

P1892 [BOI2003]团伙

扩展域并查集的经典套路,即给每个节点开一个虚拟节点,那么两个人有冲突,相当于这个人和另一个人的补集是朋友


P3295 [SCOI2016]萌萌哒

这回可真的是涨知识了,居然可以用二进制拆分的思想维护并查集
因为题目要求做的操作是区间对应连边,但是一个一个连边显然不现实,那么可以用类似于倍增的思想,每一个并查集维护一个 \(2\) 的幂次的区间,那么每次操作只需要拆分成 \(log\) 个区间的连边即可

代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
int cnt,ans,fa[maxn][25],n,m,l,r,ll,rr;
const int mod=1e9+7;
int read(){
	int x=0,f=1;
	char ch=getchar();
	while(!isdigit(ch)){
		if(ch=='-')f=-1;
		ch=getchar();
	}
	while(isdigit(ch)){
		x=x*10+ch-48;
		ch=getchar();	
	}
	return x*f;
}
int find(int x,int p){
	return fa[x][p]==x?x:fa[x][p]=find(fa[x][p],p);	
}
void merge(int x,int y,int p){
	int xx=find(x,p);
	int yy=find(y,p);
	if(xx!=yy){
		fa[xx][p]=fa[yy][p];
	}
	return ;
}
int po(int a,int b){
	int ans=1;
	while(b){
		if(b&1)ans=1ll*ans*a%mod;
		a=1ll*a*a%mod;
		b>>=1;
	}
	return ans;
}
int main(){
	n=read();
	m=read();
	for(int i=1;i<=n;i++){
		for(int j=0;j<=20;j++){
			fa[i][j]=i;
		}
	}
	for(int i=1;i<=m;i++){
		l=read();
		r=read();
		ll=read();
		rr=read();
		for(int j=20;j>=0;j--){
			if(l+(1<<j)-1<=r){
				merge(l,ll,j);
				l+=(1<<j);
				ll+=(1<<j);
			}
		}
	}
	for(int j=20;j>=1;j--){
		for(int i=1;i<=n;i++){
			if(i+(1<<j)-1>n)break;
			merge(i,find(i,j),j-1);
			merge(i+(1<<(j-1)),find(i,j)+(1<<(j-1)),j-1);
		}
	}
	for(int i=1;i<=n;i++){
		if(fa[i][0]==i)cnt++;
	}
	ans=9ll*po(10,cnt-1)%mod;
	cout<<ans;
	return 0;
}

P2391 白雪皑皑

这似乎是一个很常用的科技?
可以倒序枚举,这样每个点只需要被最后一次操作更新即可,也就是说每个点至多只会被更新一次
那么用并查集维护当前点的右边第一个没被更新的点是哪个,枚举时只需要 \(i=find(i+1)\) 即可,修改完当前点后 \(fa[i]=i+1\)

注意这种方式的常数还是非常大的,在使用之前思考能否用打标记的方法代替
比如改成每次覆盖一个子树,那么显然用这种方式改成 \(dfs\) 序上的事情并不划算,这是手动去掉了原问题自带的良好性质

代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
int n,m,p,q,l,r,fa[maxn],ans[maxn];
int find(int x){return fa[x]==x?x:fa[x]=find(fa[x]);}
int main(){
	cin>>n>>m>>p>>q;
	for(int i=1;i<=n+1;i++)fa[i]=i;
	for(int i=m;i>=1;i--){
		l=(i*p+q)%n+1,r=(i*q+p)%n+1;if(l>r)l^=r^=l^=r;
		l=find(l);while(l<=r)ans[l]=i,l=find(fa[l]=l+1);
	}
	for(int i=1;i<=n;i++)printf("%d\n",ans[i]);
	return 0;
}

边带权并查集

边带权并查集的每个父子关系都是有边权的,那么这样就可以方便地查出儿子到祖先的距离
但是注意路径压缩的时候因为父子关系变了,那么相应的边权也要发生改变,代码这样实现:

if(fa[x]!=x){
	int pre=fa[x];
	fa[x]=find(fa[x]);
	dis[x]+=dis[pre];
}
return fa[x];

P1196 [NOI2002] 银河英雄传说

这道题算是模板了,直接用并查集模拟题意即可

代码
#include<bits/stdc++.h>
using namespace std;
int n,father[30005],size[30005],dis[30005],x,y;
char c;
int find(int x){
	if(father[x]==x)return x;
	int pre=father[x],ans=find(father[x]);
	dis[x]+=dis[pre];
	father[x]=ans;
	return father[x];
}
int main(){
	cin>>n;
	for(int i=1;i<=30000;i++){
		father[i]=i;
		size[i]=1;
	}
	for(int i=1;i<=n;i++){
		scanf("\n");
		c=getchar();
		cin>>x>>y;
		if(c=='M'){
			int r1=find(x);
			int r2=find(y);
			father[r2]=r1;
			dis[r2]+=size[r1];
			size[r1]+=size[r2];
		}
		else{
			int r1=find(x);
			int r2=find(y);
			if(r1!=r2){
				//cout<<r1<<" "<<r2<<endl;
				cout<<-1<<endl;
			}
			else{
				cout<<abs(dis[x]-dis[y])-1<<endl;
			}
		}
	}
	return 0;
}

P2024 [NOI2001] 食物链

因为相同关系为长度为 \(3\) 的链,那么在并查集上将所有距离都模 \(3\) 即可


P2294 [HNOI2005]狡猾的商人

对于一个区间 \([l,r]\),利用前缀和的思想,从 \(r\) 向 \(l-1\) 连边权为 \(w\) 的边,每次并查集判断合法
当然也可以用差分约束来做

代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=105;
int fa[maxn],dis[maxn],t,n,m,x,y,w,xx,yy;
int read(){
	int x=0,f=1;
	char ch=getchar();
	while(!isdigit(ch)){
		if(ch=='-')f=-1;
		ch=getchar();
	}
	while(isdigit(ch)){
		x=x*10+ch-48;
		ch=getchar();
	}
	return x*f;
}
int find(int x){
	if(fa[x]==x)return x;
	int p=find(fa[x]);
	dis[x]+=dis[fa[x]];
	fa[x]=p;
	return p;
}
int main(){
	t=read();
	while(t--){
		n=read();
		m=read();
		for(int i=0;i<=n;i++){
			fa[i]=i;
			dis[i]=0;
		}
		bool flag=false;
		for(int i=1;i<=m;i++){
			x=read();
			y=read();
			w=read();
			xx=find(x-1);
			yy=find(y);
			if(xx==yy){
				if(w!=dis[x-1]-dis[y])flag=true;//,cout<<i<<endl;
			}
			else{
				fa[xx]=yy;
				dis[xx]=dis[y]+w-dis[x-1];
			}
		}
		if(flag)puts("false");
		else puts("true");
	}
	return 0;
}

可持久化并查集

其实就是用主席树暴力模拟所有并查集的操作了,不能路径压缩,维护出按秩合并用到的 \(fa\) 和 \(dep\) 然所有操作多带一个 \(log\) 暴力模拟就行了
当年的主席树就是丑啊

代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=(2e5+5)*20;
int n,m,fa[maxn],lson[maxn],rson[maxn],dep[maxn],tot,root[maxn],op,a,b;
int read(){
	int x=0,f=1;
	char ch=getchar();
	while(!isdigit(ch)){
		if(ch=='-')f=-1;
		ch=getchar();
	}
	while(isdigit(ch)){
		x=x*10+ch-48;
		ch=getchar();	
	}
	return x*f;
}
void build(int &p,int l,int r){
	p=++tot;
	if(l==r){
		fa[p]=l;
		return ;
	}
	int mid=l+r>>1;
	build(lson[p],l,mid);
	build(rson[p],mid+1,r);
	return ;
}
void change(int &p,int l,int r,int x,int k){
	fa[++tot]=fa[p];
	lson[tot]=lson[p];
	rson[tot]=rson[p];
	dep[tot]=dep[p];
	p=tot;
	if(l==r){
		fa[p]=k;
		return ;
	}
	int mid=l+r>>1;
	if(x<=mid)change(lson[p],l,mid,x,k);
	else change(rson[p],mid+1,r,x,k);
	return ;
}
void add(int p,int l,int r,int x,int val){
	if(l==r){
		dep[p]+=val;
		return ;	
	}
	int mid=l+r>>1;
	if(x<=mid)add(lson[p],l,mid,x,val);
	else add(rson[p],mid+1,r,x,val);
	return ;
}
int ask(int p,int l,int r,int x){
	if(l==r)return p;
	int mid=l+r>>1;
	if(x<=mid)return ask(lson[p],l,mid,x);
	else return ask(rson[p],mid+1,r,x);	
}
int find(int pos,int x){
	int p=ask(root[pos],1,n,x);
	if(fa[p]==x)return p;
	return find(pos,fa[p]);
}
void merge(int pos,int x,int y){
	int xx=find(pos,x),yy=find(pos,y);
	if(xx==yy)return ;
	if(dep[xx]>dep[yy])swap(xx,yy);
	change(root[pos],1,n,fa[xx],fa[yy]);
	add(root[pos],1,n,yy,1);
	return ;	
}
int main(){
	n=read();
	m=read();
	build(root[0],1,n);
	for(int i=1;i<=m;i++){
		op=read();
		switch(op){
			case 1: 
				a=read();
				b=read();
				root[i]=root[i-1];
				merge(i,a,b);
				break;
			case 2:
				a=read();
				root[i]=root[a];
				break;
			case 3:
				a=read();
				b=read();
				root[i]=root[i-1];
				int x=find(i,a);
				int y=find(i,b);
				if(x==y)puts("1");
				else puts("0");
		}
	}
	return 0;
}

标签:ch,return,int,查集,fa,maxn,find
来源: https://www.cnblogs.com/yang-cx/p/15550674.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有