并查集

关于并查集的应用

1.luogu P3631 [APIO2011] 方格染色

(带权并查集维护连通性)

红色为1,白色为0.观察题目所给的要求,红色为奇数个,显然,此时一个子方格中的四个数的异或值必然为1。

这个性质并无直接的效果助于做题。所以我们考虑推广这个性质

此时,有两个子方格,这两个子方格的右和左半部重合。不难发现这两个子方格的异或值为1(在满足题目的性质情况下)。同时重合的部分的异或值显然为0.

再根据0a=a,我们可以知道,两个子方格的异或值就等于,这两个方格组成的大方格的四个角的异或值.

于是,按照这样的组合方式,我们可以不断利用子方格重叠,组成一个大矩形,这个矩形的异或值就等于四个角的数的异或值.

当组成的子方格为奇数时,异或值为1.为偶数时,异或值为0.

这时,我们考虑描述已知状态.

不难发现,当这个方格的第一行和第一列确定时,整个方格便确定了,于是,染色的方案数,就是第一行和第一列的染色数.

不难想到第一个想法

暴力枚举第一行和第一列的状态,然后check,是否与数据符合,统计.

显然这个方法会T.考虑换一种更好的描述.我们可以发现当(1,1)确定时,已知方格(x,y)和(1,1)会对(1,y),(x,1)的关系进行约束,就可以利用关系列出方程.

显然,这个大方格的异或值是可以计算的,可以算出组成他的小方格,然后判断奇偶.

于是我们就有了k个形如ab=0/1的方程(有的方格未被约束,可以看成a=a这样的方程,可以取0/1),方程的解数就是方案数.

解方程将行和列的点看成二分图的点,有关系的就连边,边权为其异或值,显然会有多个连通块,且连通块之间是独立的,单个连通块的内部的解数是0/2.

枚举每条边,用带权并查集维护(维护的过程可看代码),即可check是否有解,最后的答案就是每个连通块的解数的乘积.

总结:这道题主要部分就是列方程,是利用题目给的已知色块,加上性质,对第一行和第一列进行约束,列出方程.

最后利用带权并查集维护求解.是一道不错的小思考题(本人菜)

细节很多,慢慢调

代码:(及其丑陋)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
int con;
int n,m,k;
int x[maxn],y[maxn],c[maxn];
long long ans=0;
struct node{
	int p,w;	
}e[maxn*3];
int rd(){
	int x=0,f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-') f=-1;
		ch=getchar();	
	}
	while(ch>='0'&&ch<='9'){
		x=(x<<1)+(x<<3)+ch-'0';
		ch=getchar();	
	}
	return x*f;
}
long long ksm(long long a,long long b){
	long long ans=1;
	while(b>0){
		if(b&1){
			ans*=a;
			ans%=1000000000;
		}
		a*=a;
		a%=1000000000;
		b>>=1;
	}
	return ans;
}
int find(int x){
	if(e[x].p==x) return x;
	int t=find(e[x].p);
	e[x].w^=e[e[x].p].w;
	return e[x].p=t;
}
int main(){
	n=rd();m=rd();k=rd();
	con=n;
	int po=-1;
	for(int i=1;i<=k;i++){
		x[i]=rd();y[i]=rd();c[i]=rd();
		if(x[i]==1&&y[i]==1) po=c[i];
//		y[i]+=con;
	}
	int flag=0;
	if(po!=1){
	for(int i=1;i<=n+m;i++) e[i].p=i,e[i].w=0;
	e[n+1].p=1;
	for(int i=1;i<=k;i++){//c[1][1]=0
		if(x[i]==1&&y[i]==1){
			continue;	
		}
		int o=(x[i]-1)*(y[i]-1);
		int d;
		int fx=find(x[i]),fy=find(y[i]+n);
		if(o%2==1) d=(c[i]+1)%2;
		if(o%2==0) d=c[i];
		if(fx!=fy){
			e[fx].p=fy;
			int x1=e[x[i]].w^e[y[i]+n].w;
			e[fx].w=1;
			if(x1^1!=d) e[fx].w=0;
		}
		else{
			if(e[x[i]].w^e[y[i]+n].w!=d){
				flag=1;
				break;
			}
		}
	}
	if(flag==0){
		long long sum=0;
		for(int i=1;i<=n+m;i++) if(find(i)==i) sum++;
		ans+=ksm(2,sum-1);
		ans%=1000000000;
	}
	}
	
	
	if(po!=0){
	for(int i=1;i<=n+m;i++) e[i].p=i,e[i].w=0;
	e[n+1].p=1;
	int flag=0;
	for(int i=1;i<=k;i++){//c[1][1]=1
		if(x[i]==1&&y[i]==1){
			continue;	
		}
		int fx=find(x[i]),fy=find(y[i]+n);
		int o=(x[i]-1)*(y[i]-1);
		int d;
		if(o%2==1) d=c[i];
		if(o%2==0) d=(c[i]+1)%2;
		if(fx!=fy){
			e[fx].p=fy;
			int x1=e[x[i]].w^e[y[i]+n].w;
			e[fx].w=1;
			if(x1^1!=d) e[fx].w=0;
		}
		else{
			if(e[x[i]].w^e[y[i]+n].w!=d){
				flag=1;
				break;
			}
		}
	}	
	if(flag==0){
		long long sum=0;
		for(int i=1;i<=n+m;i++) if(find(i)==i) sum++;
		ans+=ksm(2,sum-1);
	}
	}
	cout<<ans%1000000000;
	return 0;
}

2.CF734E Anton and Tree

(并查集缩点)

一道小思维题,显然缩点之后,操作必然最优,优先考虑缩点。

此时我们发现缩点后,每一个相邻节点的颜色必然不相同。这时若我们单独将任意两点以及他们之间的路径的点拿出来。
不难发现,必然是树的直径需要的操作次数最多。显然对于任意一条链来说,必然是从中间开始染色(因为全程都是扩展了两个点)最优。

考虑先对直径进行染色(所需操作次数最多),即无论如何,染色次数最起码都是直径的染色次数。

当我们染色完直径时,我们在这个过程中,一定可以顺带把直径上的点的每一个分点也染色完。

是否可能直径上某个点的分点未被染色完?由于直径的性质可知(直径长),显然不会。(读者可自行证明)

做法:利用并查集缩点,求出直径,答案就是直径的节点数除以2(可以找找规律,或者直接推)

补充一个关于树的直径的小性质:从树的任意一个节点,找出距离他最远的点x,点x必然是直径上的一个端点。
可用反证法证明,证明见OI-Wiki


代码树的直径不会求):

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+10;
int n,color[maxn],fa[maxn];
int u[maxn],v[maxn],cnt,cnt1;
int dep[maxn];
int ver[maxn];
vector<int> q[maxn];
int rd(){
	int x=0,f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		x=(x<<1)+(x<<3)+ch-'0';
		ch=getchar();	
	}
	return x*f;
}
bool cmp(int x,int y){
	return x>y;	
}
int find(int x){
	if(fa[x]==x) return x;
	return fa[x]=find(fa[x]);	
}
void dfs(int u,int f){
	dep[u]=dep[f]+1;
	if(dep[u]>cnt){
		cnt=dep[u];
		cnt1=u;
	}
	for(int i=0;i<q[u].size();i++){
		int v=q[u][i];
		if(v==f) continue;
		dfs(v,u);
	}
}
int main(){
	n=rd();
	for(int i=1;i<=n;i++) color[i]=rd();
	for(int i=1;i<=n;i++) fa[i]=i;
	for(int i=1;i<n;i++){
		u[i]=rd();v[i]=rd();
		if(color[u[i]]==color[v[i]]){
			int fx=find(u[i]),fy=find(v[i]);
			fa[fx]=fy;
		}
	}
	for(int i=1;i<n;i++){
		int fx=find(u[i]),fy=find(v[i]);
		if(fx!=fy){
			q[fx].push_back(fy);
			q[fy].push_back(fx);
		}
	}
	dep[0]=-1;
	dfs(find(u[1]),0);
	memset(dep,0,sizeof(dep));
	dep[0]=-1;
	dfs(cnt1,0);
//	int cnt=0;
//	for(int i=1;i<=n;i++)
//		if(i==find(i)) ver[++cnt]=dep[i];
//	sort(ver+1,ver+cnt+1,cmp);
	cout<<(cnt+1)/2;
	return 0;	
}

3.P2391 白雪皑皑

(并查集维护序列后继)

不难发现,这道题中,染色操作编号越大优先度越高.于是考虑从大到小进行染色操作.

若直接进行暴力染色,复杂度为O(NM).这时我们注意到,由于染色的优先性,从大到小染色,已经被染色的就不用进行第二次染色了.

于是,自然的想到可以对已经染色的位置打好标记.同时为了确定未被染色的位置,我们可以用并查集维护当前这个位置的右边第一个没有染色的位置.

这样,每一个位置都只会染色一次,那么染色操作的均摊复杂度就是O(1).所以最后的时间复杂度就优化成了O(M)

小细节:fa数组应预处理到n+1位,否则可能一直遍历0-n+1,就会gg

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
int n,m,p,q;
int color[maxn],fa[maxn];
int find(int x){
	if(fa[x]==x) return x;
	return fa[x]=find(fa[x]);	
}
int main(){
	cin>>n>>m>>p>>q;
	for(int i=1;i<=n+1;i++) fa[i]=i;
	int l,r;
	for(int i=m;i>=1;i--){
		l=(i*p+q)%n+1,r=(i*q+p)%n+1;
		if(l>r) swap(l,r);
		l=find(l);
		int fy=find(r+1);
		while(l<=r){
			color[l]=i;
			fa[l]=fy;
			l=find(l+1);
		}
	}
	for(int i=1;i<=n;i++) cout<<color[i]<<endl;
	return 0;	
}
posted @   sin_wt  阅读(38)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示