并查集
并查集是一种用于管理元素所属集合的数据结构,实现为一个森林,其中每棵树表示一个集合,树中的节点表示对应集合中的元素。
顾名思义,并查集支持两种操作:
-
合并(Union):合并两个元素所属集合(合并对应的树)
-
查询(Find):查询某个元素所属集合(查询对应的树的根节点),这可以用于判断两个元素是否属于同一集合
常见并查集:普通并查集,扩展域并查集,带权并查集,可持久化并查集,可撤销并查集。
P3367 【模板】并查集
普通并查集
无优化,会T3个点
#include<iostream>
#include<cstdio>
#define MAXN 10000
using namespace std;
int n,m,p,x,y;
int opt;
int fa[MAXN];
int find(int x)
{
if(x==fa[x]) return x;
return find(fa[x]);
}
void join(int c1,int c2)
{
int f1=find(c1),f2=find(c2);
if(f1!=f2) fa[f1]=f2;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i)
{
fa[i]=i;
}
for(int i=1;i<=m;i++)
{
scanf("%d",&opt);
if(opt==1
){
scanf("%d %d",&x,&y);
join(x,y);
}
else if(opt==2)
{
scanf("%d %d",&x,&y);
if(find(x)==find(y)) cout<<"Y"<<endl;
else cout<<"N"<<endl;
}
}
return 0;
}
路径压缩优化
#include<iostream>
#include<cstdio>
#define MAXN 20005
using namespace std;
int n,m,p,x,y;
int opt;
int fa[MAXN];
int find(int x)
{
if(x==fa[x]) return x;
return fa[x]=find(fa[x]);
}
void join(int c1,int c2)
{
int f1=find(c1),f2=find(c2);
if(f1!=f2) fa[f1]=f2;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i)
{
fa[i]=i;
}
for(int i=1;i<=m;i++)
{
scanf("%d",&opt);
if(opt==1
){
scanf("%d %d",&x,&y);
join(x,y);
}
else if(opt==2)
{
scanf("%d %d",&x,&y);
if(find(x)==find(y)) cout<<"Y"<<endl;
else cout<<"N"<<endl;
}
}
return 0;
}
按秩合并
#include<iostream>
#include<cstdio>
#define MAXN 10005
using namespace std;
int n,m,p,x,y;
int opt;
int fa[MAXN],siz[MAXN];
int find(int x)
{
if(x==fa[x]) return x;
return fa[x]=find(fa[x]);
}
void join(int c1,int c2)
{
int f1=find(c1),f2=find(c2);
if(f1!=f2)
{
if(siz[f1]>siz[f2])
{
fa[f1]=f2;
siz[f2]+=siz[f1];
}
else
{
fa[f2]=f1;
siz[f1]+=siz[f2];
}
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i)
{
fa[i]=i;
siz[i]=1;
}
for(int i=1;i<=m;i++)
{
scanf("%d",&opt);
if(opt==1
){
scanf("%d %d",&x,&y);
join(x,y);
}
else if(opt==2)
{
scanf("%d %d",&x,&y);
if(find(x)==find(y)) cout<<"Y"<<endl;
else cout<<"N"<<endl;
}
}
return 0;
}
路径压缩与按秩合并均可 A C AC AC,但路径压缩会快个 20 m s 20ms 20ms 左右。
P1551 亲戚
纯板子
P1892 [BOI2003] 团伙
扩展域并查集的使用。令 f a 1 − f a n fa_1-fa_n fa1−fan 表示朋友域, f a n + 1 − f a 2 n fa_{n+1}-fa_{2n} fan+1−fa2n 表示敌人域, F F F 操作直接合并即可。 E E E 操作将 p p p 与 q + n q+n q+n , q q q 与 p + n p+n p+n 合并(自己与自己敌人的敌人是朋友) ,一定要将敌人域接到朋友域上来,否则答案会少!
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int maxn=2e3+5;
int fa[maxn];
int n,m;
char opt;int p,q;
void init()
{
for(int i=1;i<=2*n;i++) fa[i]=i;
}
int find(int x)
{
if(fa[x]==x) return fa[x];
return fa[x]=find(fa[x]);
}
void join(int c1,int c2)
{
int x1=find(c1),x2=find(c2);
if(x1!=x2) fa[x2]=x1;
}
int main()
{
#ifndef ONLINE_JUDGE
//freopen("in.txt","r",stdin);
#endif
cin>>n>>m;
init();
for(int i=1;i<=m;i++)
{
cin>>opt>>p>>q;
if(opt=='F')
{
join(p,q);
}
else join(p,q+n),join(q,p+n);
}
int cnt=0;
for(int i=1;i<=n;i++) if(fa[i]==i) cnt++;
cout<<cnt;
return 0;
}
P1196 [NOI2002] 银河英雄传说
带权并查集。带权并查集特征:多维护一个数组 d d d , d x d_x dx 表示 x x x 到根节点的权值。合并和路径压缩时注意如何更新维护的数组。
路径压缩关键代码:
写法1
int find(int x)
{
if(x!=fa[x])
{
int rt=find(x);//直接找到根节点接上去
d[x]+=d[fa[x]];
fa[x]=rt;
}
return fa[x];
}
写法2
int find(int x)
{
if(x!=fa[x])
{
int t=fa[x];
fa[x]=find(fa[x]);
d[x]+=d[t];
}
return fa[x];
}
于是此题考虑维护两个数组。其中 d d d 表示该战舰到根的距离, s s s 表示战舰群的长度,这样处理答案显然。处理过程如图:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int maxn=30005;
int T;
int fa[maxn],d[maxn],s[maxn];
char op;int s1,s2;
void init()
{
for(int i=1;i<maxn;i++) fa[i]=i,s[i]=1;
}
int find(int x)
{
if(x!=fa[x])
{
int t=fa[x];
fa[x]=find(fa[x]);
d[x]+=d[t];
}
return fa[x];
}
void join(int s1,int s2)
{
int x1=find(s1),x2=find(s2);
if(x1!=x2)
{
d[x1]=s[x2];
s[x2]+=s[x1];
fa[x1]=x2;
}
}
int main()
{
#ifndef ONLINE_JUDGE
freopen("in.txt","r",stdin);
#endif
cin>>T;
init();
while(T--)
{
cin>>op>>s1>>s2;
if(op=='M')
{
join(s1,s2);
// for(int i=1;i<=n;i++) cout<<fa[i]<<" ";cout<<endl;
// for(int i=1;i<=n;i++) cout<<d[i]<<" ";cout<<endl;
// for(int i=1;i<=n;i++) cout<<s[i]<<" ";cout<<endl;
}
else
{
int x1=find(s1),x2=find(s2);
if(x1!=x2) cout<<-1<<endl;
else cout<<max(abs(d[s1]-d[s2])-1,0)<<endl;
}
}
return 0;
}
P1955 [NOI2015] 程序自动分析
主要思想:相等的数在同一个集合,不相等的数在一个集合就会有矛盾。
离散化+并查集即可。
P1197 [JSOI2008] 星球大战
经典倒序处理+连通块判断。
因为并查集的删除操作不好做,于是考虑从最后情况开始,不断建边直到初始状况,记录连通块数量便是答案。
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>
using namespace std;
const int maxn=2e6+5;
int n,m,k;
int fa[maxn],ans[maxn];
bool f[maxn];
int brok[maxn];
void init()
{
for(int i=0;i<=n;i++)
{
fa[i]=i;
}
}
int find(int x)
{
if(x==fa[x]) return x;
return fa[x]=find(fa[x]);
}
int tot;
void join(int c1,int c2)
{
int f1=find(c1),f2=find(c2);
if(f1!=f2)
{
--tot;fa[f1]=f2;//每一次合并操作减少1个连通块
}
}
vector<int> G[maxn];
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int x,y;
scanf("%d%d",&x,&y);
G[x].push_back(y);
G[y].push_back(x);
}
cin>>k;
for(int i=1;i<=k;i++)
{
scanf("%d",&brok[i]);
f[brok[i]]=1;
}
init();
tot=n-k;
// cout<<tot<<endl;
for(int i=0;i<n;i++)
{
if(!f[i])
{
for(int j=0;j<G[i].size();j++)
{
if(!f[G[i][j]])
{
join(i,G[i][j]);
// --tot;
}
}
}
}
for(int i=k;i>=1;i--)
{
ans[i]=tot;
int now=brok[i];
++tot,f[now]=0;//倒推,增加一个连通块
for(int j=0;j<G[now].size();j++)
{
int v=G[now][j];
if(f[v])
{
continue;
}
join(now,v);
// --tot;
// tot=max(1,tot);
}
}
ans[0]=tot;
for(int i=0;i<=k;i++) cout<<ans[i]<<endl;
return 0;
}
4287. 导航噩梦
-
题目大意:
给定 n n n 个坐标的 m m m 个相对关系,求出输入的第 i i i 条语句时两个坐标的曼哈顿距离。
-
题目分析:
首先关于每个农场是否连通,可用普通并查集解决。
而维护每个点之间的曼哈顿距离,则想到用带权并查集,使用数组 d x , d y dx,dy dx,dy 分别维护点 i i i 到跟节点的横纵坐标之差 d x i , d y i dx_i ,dy_i dxi,dyi。
本题要求到第 i i i 条信息后的情况,考虑询问按照 i i i 排序,离线处理。
-
具体实现:
基本思路已经明确了,接下来就是看如何操作,主要是看怎样维护带权并查集。
这里都是向量的运算,建议熟悉这方面知识。
-
修改操作
以 d = E d=\texttt{E} d=E 时举例:假设 a a a 节点的祖先为 p a pa pa, b b b 节点的祖先为 p b pb pb 。
我们不妨将 p a pa pa 集合合并到 p b pb pb 上。此时便考虑如何更新 d x p a , d y p a {dx}_{pa},{dy}_{pa} dxpa,dypa。
根据下面这张图
可以得出此时 d x p a = d x b + l − d x a {dx}_{pa}={dx}_b+l-dx_a dxpa=dxb+l−dxa 。
d y dy dy及剩余的操作同理可以得出。
-
路径压缩
根据此图,将 f a x fa_x fax 直接合并到根节点上,此时接可以直接更新 d x , d y d_x,d_y dx,dy 了。
dx[x]+=dx[fa[x]]; dy[x]+=dy[fa[x]];
将 x x x 到根的距离分成两段求有以上公式。
-
-
AC_code
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn=40005;
int n,m,k;
char d;
int fa[maxn],dx[maxn],dy[maxn];
void init(int n)
{
for(int i=1;i<=n;i++)
{
fa[i]=i,dx[i]=0,dy[i]=0;
}
}
int find(int x)//路径压缩
{
if(fa[x]!=x)
{
int rt=find(fa[x]);
dx[x]+=dx[fa[x]];
dy[x]+=dy[fa[x]];
fa[x]=rt;
}
return fa[x];
}
struct node
{
int a,b,i;
int id;
}qus[maxn];
bool cmp(node a,node b)
{
return a.i<b.i;//依照i排序
}
struct opt
{
int a,b,l;
char d;
}op[maxn];
int ans[maxn],cnt=0;
int main()
{
#ifndef ONLINE_JUDGE
freopen("in.txt","r",stdin);
#endif
cin>>n>>m;
init(n);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&op[i].a,&op[i].b,&op[i].l);
cin>>op[i].d;
}
cin>>k;
for(int i=1;i<=k;i++)
{
scanf("%d%d%d",&qus[i].a,&qus[i].b,&qus[i].i);
qus[i].id=i;
}
//读入,方便离线处理
sort(qus+1,qus+k+1,cmp);
int now=1;
for(int i=1;i<=k;i++)
{
while(now<=qus[i].i)
{
char d=op[now].d;
int a=op[now].a,b=op[now].b,l=op[now].l;
int pa=find(a),pb=find(b);
if(pa==pb) continue;
if(d=='E')//依照dx,dy的定义合并+维护
{
fa[pa]=pb;
dx[pa]=dx[b]+l-dx[a];
dy[pa]=dy[b]-dy[a];
}
else if(d=='S')
{
fa[pa]=pb;
dy[pa]=dy[b]+l-dy[a];
dx[pa]=dx[b]-dx[a];
}
else if(d=='W')
{
fa[pa]=pb;
dx[pa]=dx[b]-l-dx[a];
dy[pa]=dy[b]-dy[a];
}
else if(d=='N')
{
fa[pa]=pb;
dy[pa]=dy[b]-l-dy[a];
dx[pa]=dx[b]-dx[a];
}
now++;
}
//按照i的顺序离线处理(离线的原因之一是并查集的删除操作并不好做)
int a=qus[i].a,b=qus[i].b;
int pa=find(a),pb=find(b);
if(pa!=pb)
{
ans[qus[i].id]=-1;
}
else
{
ans[qus[i].id]=abs(dx[a]-dx[b])+abs(dy[a]-dy[b]);
}
}
for(int i=1;i<=k;i++)
{
cout<<ans[i]<<endl;
}
return 0;
}
P4185 [USACO18JAN] MooTube G
同样并查集+连通块。