并查集
关于并查集的应用
1.luogu P3631 [APIO2011] 方格染色
(带权并查集维护连通性)
红色为1,白色为0.观察题目所给的要求,红色为奇数个,显然,此时一个子方格中的四个数的异或值必然为1。
这个性质并无直接的效果助于做题。所以我们考虑推广这个性质。
此时,有两个子方格,这两个子方格的右和左半部重合。不难发现这两个子方格的异或值为1(在满足题目的性质情况下)。同时重合的部分的异或值显然为0.
再根据,我们可以知道,两个子方格的异或值就等于,这两个方格组成的大方格的四个角的异或值.
于是,按照这样的组合方式,我们可以不断利用子方格重叠,组成一个大矩形,这个矩形的异或值就等于四个角的数的异或值.
且当组成的子方格为奇数时,异或值为1.为偶数时,异或值为0.
这时,我们考虑描述已知状态.
不难发现,当这个方格的第一行和第一列确定时,整个方格便确定了,于是,染色的方案数,就是第一行和第一列的染色数.
不难想到第一个想法
暴力枚举第一行和第一列的状态,然后check,是否与数据符合,统计.
显然这个方法会T.考虑换一种更好的描述.我们可以发现当(1,1)确定时,已知方格(x,y)和(1,1)会对(1,y),(x,1)的关系进行约束,就可以利用关系列出方程.
显然,这个大方格的异或值是可以计算的,可以算出组成他的小方格,然后判断奇偶.
于是我们就有了k个形如的方程(有的方格未被约束,可以看成这样的方程,可以取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 白雪皑皑
(并查集维护序列后继)
不难发现,这道题中,染色操作编号越大优先度越高.于是考虑从大到小进行染色操作.
若直接进行暴力染色,复杂度为.这时我们注意到,由于染色的优先性,从大到小染色,已经被染色的就不用进行第二次染色了.
于是,自然的想到可以对已经染色的位置打好标记.同时为了确定未被染色的位置,我们可以用并查集维护当前这个位置的右边第一个没有染色的位置.
这样,每一个位置都只会染色一次,那么染色操作的均摊复杂度就是.所以最后的时间复杂度就优化成了
小细节: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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】