并查集

一般对于连通性问题,并查集是非常好用的
不仅仅可以维护出是否连通,还可以顺便维护出当前连通块的一些信息,比如直径
有时候并查集的祖先节点不做区分,即合并是认定父子关系是随意的,但是是遇到构建 \(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;
}
posted @ 2022-07-22 07:57  y_cx  阅读(99)  评论(1编辑  收藏  举报