图论专题-学习笔记:并查集
1.概述
并查集是一种数据结构,用于图论之中(更多时候用于树),通常用来维护一张无向图内 \(O(1)\) 判断两个点是否在同一个连通块内,有很多的用法,扩展性也比较高。
2.模板
下面还是通过一道模板讲解并查集的用法。
我们假设这 4 个元素分别表示 4 个人。假设每个人都会在一个群内,第 \(i\) 个人的群主表示为 \(fa_i\) (其实如果抽象成一棵树,就是 \(i\) 的父亲节点)
初始时,每一个人单独在一个群内,则令 \(fa_i=i\) (这一步是并查集的初始化,非常重要,否则所有人的群主/每个点的祖先就都变成了不存在的 0 号节点,后续操作就会出现很多奇奇怪怪的错误)
看操作 1 :问第 1 个人与第 2 个人在不在同一个群内。
显然不在了,大家各管各的,输出 N
。
操作 2:合并第 1 个人与第 2 个人所在的群。
如何合并呢?此时两个人分属于不同的群,现在要将两个人合成一个群,那么我们直接把 2 的群主改成 1 不就可以了?即令 \(fa_2=1\) 。从树的角度看,初始时每一个点都是单独的根节点,现在在 1 和 2 之间连一条边,生成一棵新树,同时令 1 为根节点。
接下来又问 1 与 2 在不在一个群内。
这一步是并查集的判断操作,判断时我们发现,\(fa_1=fa_2\) ,那么他们在一个群内,输出 Y
。
此时群组情况如下所示:
1 3 4
\
2
接下来合并 3 4.仿照上述步骤,令 \(fa_4=3\) 。
这里说明一下,其实令 \(fa_3=4\) 也是可以的,看个人习惯,本质上并没有什么区别,毕竟都在一个群里面,谁是群主都没有问题。
群组情况 :
1 3
\ \
2 4
下一个操作询问 1 4 在不在一个群内,\(fa_1=1\),\(fa_3=3\) ,群主不一样,不在一个群内,输出 N
。
下一步合并 2 3,此时。。。。。。
1 不同意了!如果我们修改 \(fa_3=2\) ,没有什么问题(具体为什么见下文),但是万一程序让 \(fa_2=3\) ,那么 1 就不同意了:“ 2 明明在我的群,凭什么到你的群去了?”怎么办?
既然 2 搞不定,我们直接找最高群主 1 谈谈,直接令 \(fa_1=3\) 就可以解决了。从树的角度看,就是改变根节点的父亲。
群主情况:
3
/ \
1 4
\
2
接下来问 1 4 在不在一个群内, \(fa_1=fa_4\) ,在一个群内,输出 Y
。
然而此时,如果再来一个询问:询问 2 4在不在一个群内,要怎么办呢?
肉眼可见, 2 4 在一个群内,应该输出 Y
,然而我们上面的判断都是根据 \(fa_i\) 是否相等判断的,此时并不相等,不就出问题了吗?
为了解决这个问题,方法是:找到最高群主也就是根节点。
看图,2 的群主是 1 ,而 1 的群主是 3 ,这样 2 的最高群主不就是 3 了吗?4 的最高群主也是 3 ,在同一个群内。
如果此时又来一个问题:判断现在有几个群要怎么办呢?
由于每一个群都有最高群主,且只有最高群主的群主是自己(为什么?),那么只要统计出有几个 \(i \in [1,n]\) 使得 \(fa_i==i\) 即可。也就是找出每一棵树的根节点。
完美解决~~~
代码实现:
- 初始化:
这里直接一遍 for 即可。for(int i=1;i<=n;i++) fa[i]=i;
- 查找某个节点的最高群主也就是根节点。
递归查找即可,代码如下:
其中, \(fa_x==x\) 表示找到根节点了(根节点的父亲就是根节点,初始化时已经这样操作过了),找到返回 \(x\) ,否则递归查找。int gf(int x) {return (fa[x]==x)?x:gf(fa[x]);}
然而你以为这样就结束了吗?看下图:
这样,查询一次 \(fa_{op}\) 就要 \(O(1e5或1e6)\) 的时间复杂度,多查几次不就 TLE 了?为了解决这个问题,我们引入一个优化:路径压缩。1-2-3-4-5-6-7-......-op(某极大的数字,比如说 1e5 1e6 之类的)
路径压缩的目的就是为了解决上面的问题,即在查找某节点的祖先的时候,我们将一路上查找的所有节点的父亲全部连到根节点,也就是变成下图:
这样,查询复杂度直接降至 \(O(1)\) ,大大优化查询复杂度。1 | \ \ \ \ 2 3 4 ... op
而代码只需要这样改:int gf(int x) {return (fa[x]==x)?x:fa[x]=gf(fa[x])}
- 合并操作:
找到根节点合并即可。void hb(int x,int y) {if(gf(x)!=gf(y)) fa[fa[x]]=fa[y];} //由于加入了路径压缩所以不会有问题。
- 查询操作
判断两个点的祖先是否相同即可。cout<<((gf(x)==gf(y))?'Y':'N')<<"\n"; //实测这里三目运算符外面不加括号会CE
- 统计树的数量
根据上述所讲,一遍 for 即可。for(int i=1;i<=n;i++) if(fa[i]==i) ans++;
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e4+10;
int n,m,fa[MAXN];
int gf(int x) {return (fa[x]==x)?x:fa[x]=gf(fa[x]);}
void hb(int x,int y) {if(gf(x)!=gf(y)) fa[fa[x]]=fa[y];}
int read()
{
int sum=0,fh=1;char ch=getchar();
while(ch<'0'||ch>'9') {if(fh=='-') fh=-1;ch=getchar();}
while(ch>='0'&&ch<='9') {sum=(sum<<3)+(sum<<1)+ch-'0';ch=getchar();}
return sum*fh;
}
int main()
{
n=read();m=read();
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=m;i++)
{
int x,y,z;
z=read();x=read();y=read();
if(z==1) hb(x,y);
else cout<<((gf(x)==gf(y))?'Y':'N')<<"\n";
}
return 0;
}
如果你看懂了上述代码,那么恭喜你,学会了并查集的基础操作!
接下来,你将会见到各路例题以及并查集的各种神奇用法。
3.例题
题单:
- 入门题:
- [BOI2003]团伙
- 与别的算法结合:
- 搭配购买
- 关押罪犯
- 考思维的题:
- [JSOI2008]星球大战
- [IOI2014]game 游戏
- 二维转一维:
- [USACO14JAN]Ski Course Rating G
- 小 D 的地下温泉
- 扩展域并查集&边带权并查集:
- [NOI2001]食物链
- [NOI2002]银河英雄传说
- [CEOI1999]Parity Game
1.入门题:
这道题是一道练手题,思维与算法难度都不高,就是一个并查集。
首先处理读入数据,将是朋友的人合并,是敌人的人先存在 \(v\) 数组里面(使用 vector ,不会的请自行查百度)。
然后根据我的敌人的敌人是我的朋友,三重循环再合并一次即可。
代码(篇幅有限,只放部分代码,下同):
const int MAXN=1000+10;
int n,m,ans,fa[MAXN];
vector<int>v[MAXN];
int main()
{
n=read();m=read();
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=m;i++)
{
char op;int p,q;
cin>>op;p=read();q=read();
if(op=='F') hb(p,q);
else
{
v[p].push_back(q);
v[q].push_back(p);
}
}
for(int i=1;i<=n;i++)
for(int j=0;j<v[i].size();j++)
for(int k=0;k<v[v[i][j]].size();k++) hb(i,v[v[i][j]][k]);
for(int i=1;i<=n;i++) if(gf(i)==i) ans++;
cout<<ans<<"\n";
return 0;
}
2.与别的算法结合:
卖云朵可还行
这道题首先,要同时买两朵云的操作就很像并查集,因此我们可以考虑使用并查集来求解(通常题目当中出现了 “同时....” / “一起....” 等字眼都有可能是并查集)。
然后,又看到要买云朵,每种云朵只有一份,钱数又是有限的,浓浓的透露出 0/1 背包 的气息。
因此,本道题的算法为:并查集 + 0/1 背包
首先将必须同时购买的物品合并,然后将云朵组成的一棵棵树中所有节点的 \(c_i,d_i\) 全部加起来,放到新数组 \(money_j,value_j\) 中,跑一遍 0/1 背包即可求解。
代码:
const int MAXN=1e4+10;
int n,m,w,c[MAXN],d[MAXN],money[MAXN],value[MAXN],fa[MAXN],ys[MAXN],tmp,f[MAXN];
int main()
{
n=read();m=read();w=read();
for(int i=1;i<=n;i++) {c[i]=read();d[i]=read();fa[i]=i;}
for(int i=1;i<=m;i++)
{
int x,y;
x=read();y=read();
hb(x,y);
}//合并操作
for(int i=1;i<=n;i++) if(gf(i)==i) ys[i]=++tmp;//处理出最后物品个数
for(int i=1;i<=n;i++)
{
money[ys[fa[i]]]+=c[i];
value[ys[fa[i]]]+=d[i];
}//算出 money[i] 和 value[i]
for(int i=1;i<=tmp;i++)
for(int j=w;j>=money[i];j--)
f[j]=Max(f[j],f[j-money[i]]+value[i]);// 0/1 背包
cout<<f[w]<<"\n";
return 0;
}
这道题可以使用二分图来解,那么如何使用并查集来解呢?
由于要想办法让最大值最小,所以使用二分?
No,这道题不需要使用二分,而是贪心即可。想一想,我们只需要尽量将怒气值大的罪犯组拆掉不就好了,碰到第一个不能拆掉的就是答案。
因此,这道题的算法为:并查集 + 贪心。
首先,按照怒气值从大到小排序一遍。
然后,我们令 \(d_i\) 表示 \(i\) 的第一个会与他发生摩擦的人,初始化为 0 。
接下来处理数据。假设此时我们要处理 \(a\) 与 \(b\) 发生摩擦,怒气值为 \(c\) 的信息:
- 如果此时 \(a,b\) 已经在一起了,直接输出 \(c\) ,结束程序。
- 否则,他们不在一起,以 \(a\) 为例:如果 \(d_a=0\),说明此时没有人与他有摩擦,则 \(d_a=b\) ,否则说明已经有人与他有摩擦了,由于只有两个监狱,那么合并 \(b,d_a\) 即可。
- 正确性:显然要将 \(d_a,a\) 拆掉。假设 \(b,d_a\) 之间的怒气值(如果没有摩擦为 0)为 \(c'\) ,根据之前的排序,必然有 \(c'<c\),那么显然合并 \(b,d_a\) 比合并 \(a,b\) 更优。
代码:
const int MAXN=20000+10,MAXM=100000+10;
int n,m,fa[MAXN],d[MAXN];
struct node
{
int a,b,c;
}a[MAXM];
int main()
{
n=read();m=read();
for(int i=1;i<=m;i++) {a[i].a=read();a[i].b=read();a[i].c=read();}
for(int i=1;i<=n;i++) fa[i]=i;
sort(a+1,a+m+1,cmp);//自行打 cmp 函数
for(int i=1;i<=m;i++)
{
if(gf(a[i].a)!=gf(a[i].b))
{
if(!d[a[i].a]) d[a[i].a]=a[i].b;
else hb(d[a[i].a],a[i].b);
if(!d[a[i].b]) d[a[i].b]=a[i].a;
else hb(d[a[i].b],a[i].a);
}
else {cout<<a[i].c<<"\n";return 0;}
}
cout<<"0\n";
return 0;
}
3.考思维的题:
正常的并查集支持合并操作,但是不支持删除操作,然而这道题的所有操作不是合并就是删除,那么要怎么办呢?
既然并查集支持合并操作,那么我们想办法支持合并操作就好了呗!
我们将打击星球的顺序 倒过来操作 ,将其视作 重建星球 ,然后每重建一个合并一次,最后处理连通块个数不就好了qwq。
关于如何处理连通块个数,这里提供一个 \(O(1)\) 的思想:
- 初始化 \(sum=n\) ,表示有 \(n\) 个连通块。
- 每合并两个点,\(sum--\) 。
- 注意合并的两个点不能在同一个连通块内。
注意:最后输出答案时要逆序输出,没有重建的星球不算在答案内,只有当两个星球全部重建完成才能合并。这里我使用 \(book\) 数组标记是否合并完成。
代码:
const int MAXN=4e5+10;
int n,fa[MAXN],m,k,des[MAXN],ans[MAXN],sum;
bool book[MAXN];
vector<int>v[MAXN];
void hb(int x,int y) {if(gf(x)!=gf(y)) {sum--;fa[fa[x]]=fa[y];}}
//注意 sum--;,gf(x)略
int main()
{
n=read();m=read();
for(int i=0;i<n;i++) fa[i]=i;
for(int i=1;i<=m;i++)
{
int x=read();int y=read();
v[x].push_back(y);v[y].push_back(x);
}
k=read();sum=n;
for(int i=1;i<=k;i++) book[des[i]=read()]=1;
for(int i=0;i<n;i++)
if(!book[i])
for(int j=0;j<v[i].size();j++)
if(!book[v[i][j]]) hb(i,v[i][j]);
for(int zzh=k;zzh>=1;zzh--)
{
ans[zzh]=sum-zzh;
book[des[zzh]]=0;
for(int i=0;i<v[des[zzh]].size();i++)
if(!book[v[des[zzh]][i]]) hb(v[des[zzh]][i],des[zzh]);
}
ans[0]=sum;
for(int i=0;i<=k;i++) cout<<ans[i]<<"\n";
return 0;
}
别看这道题是 IOI 的题目,其实想通了真的非常简单。你看评级都是绿色
首先,为了让梅玉只有到最后一个询问才能判断是否连通,这里就有一种思路:我们构造某一张图使得这张图连通,且最后一个询问问的点 \(x,y\) 会连一条边,这里记作 \(x->y\) ,但是一旦我们删去了 \(x->y\) 整张图就会裂成两个集合,换句话说, \(x->y\) 是这一张图的桥/割边。(桥/割边的定义:如果删除某条 \(u->v\) 的边后途中连通块个数增加,那么 \(x->y\) 是这张图的桥/割边)
为什么正确呢?如果 \(x->y\) 是这张图的割边,那么在倒数第二个询问中梅玉依然不能判断整张图是否连通(有 2 个连通块),此时她必须再问一次才能确定图是否连通。
因此思路就很明确了,我们对于某个询问 \(a->b\) ,如果合并 \(a,b\) 以后 \(x,y\) 在一个连通块内,这显然不是我们想要的操作,此时不能合并 \(a,b\) ;否则,合并 \(a,b\) 。最后不要忘记输出最后一条边是否连通,这里输出 0
或 1
都可以。不过根据我们构造的方案,最好输出 1
。(实测 0
也能够通过)
当然,本博客只是提供了其中一种思路,具体别的思路也请各位发现然后解决。
所以你看,IOI的题也不见得非常难
代码:
const int MAXN=1500+10;
int n,m,fa[MAXN],q1[MAXN*MAXN],q2[MAXN*MAXN];
int main()
{
n=read();m=n*(n-1)/2;
for(int i=1;i<=m;i++) {q1[i]=read();q2[i]=read();}
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<m;i++)
{
int x=q1[i],y=q2[i];
int fx=gf(x),fy=gf(y);
int x1=q1[m],y1=q2[m];
int fx1=gf(x1),fy1=gf(y1);
if(fx>fy) swap(fx,fy);
if(fx1>fy1) swap(fx1,fy1);
if(fx==fx1&&fy==fy1) cout<<"0\n";//判断 最后两个点 合并之后是否在同一集合内,在就不合并
else
{
cout<<"1\n";
hb(x,y);
}//否则合并
}
cout<<"1\n";//输出 1 也可以
return 0;
}
4.二维转一维:
[USACO14JAN]Ski Course Rating G
这道题也是一道与贪心相结合的题目。
首先,我们需要将二维的地图转成一维:对于 \((i,j)\) (第 \(i\) 行第 \(j\) 列,下同),我们将其在一维编号为 \((i-1)*m+j\) (注意不是 \(n\)),然后对于相邻的两个二维的点连一条边,将点压成一维后按照边权从小到大排序。
然后,对于每一条边:
- 首先,如果这条边连着的两个点在一个连通块内,
continue;
。 - 然后,如果两棵子树 \(size\) 和大于等于 \(t\),那么 \(ans+=c*(cnt_i)\)(\(cnt_i\) 见下文)。 其中, \(i\) 为两个子树的编号,且 \(size_i<t\) 。为什么大于 \(t\) 的就不能统计了呢?因为之前 \(size_i<t\) 的时候已经统计过一次,此时又统计就会造成浪费,并且即使有新的起点加入,也已经被统计过,没有意义了。
- 而后合并两个点,如果这两个点内有起点,我们就将增加的起点个数存在 \(cnt_i\) 里面。
代码:
const int MAXN=500+10;
int t,n,m,a[MAXN][MAXN],b[MAXN][MAXN],fa[MAXN*MAXN],size[MAXN*MAXN],tmp,cnt[MAXN*MAXN];
typedef long long LL;
LL ans;//不开long long见祖宗
struct node
{
int a,b,c;
}dis[2*MAXN*MAXN];
int main()
{
//读入略,a=地图,b=是否为起点
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
if(j!=m)
{
tmp++;
dis[tmp].a=turn(i,j);dis[tmp].b=turn(i,j+1);dis[tmp].c=abs(a[i][j]-a[i][j+1]);
}
if(i!=n)
{
tmp++;
dis[tmp].a=turn(i,j);dis[tmp].b=turn(i+1,j);dis[tmp].c=abs(a[i][j]-a[i+1][j]);
}
if(b[i][j]==1) cnt[turn(i,j)]=1;
}//连边操作
for(int i=1;i<=n*m;i++) fa[i]=i,size[i]=1;
sort(dis+1,dis+tmp+1,cmp);
for(int i=1;i<=tmp;i++)
{
int fx=gf(dis[i].a),fy=gf(dis[i].b);
if(fx==fy) continue;
if(size[fy]+size[fx]>=t)
{
if(size[fy]<t) ans+=(LL)dis[i].c*cnt[fy];
if(size[fx]<t) ans+=(LL)dis[i].c*cnt[fx];
}
if(size[fx]>size[fy]) swap(fx,fy);
fa[fx]=fy;
size[fy]+=size[fx];cnt[fy]+=cnt[fx];//注意更新答案
}
cout<<ans<<"\n";
return 0;
}
这道题类似,首先二维转一维不说,然后如果两个相邻点都是泉水合并。
询问操作:直接求出询问点所在树的 \(size\) 即可,求个最大值。
有个坑点:当心所有点都是土地,此时我们需要输出 1
。
修改操作:泉水改土地直接修改地图然后 \(size--\) 即可。土地改泉水时我们需要新开一个点,改变地图后令新开的点 \(fa=自己,size=1\) ,然后四周合并一遍即可。注意不能直接在原点上修改,否则会有很多奇奇怪怪的问题。
代码:
const int MAXN=1e6+10;
int n,m,fa[MAXN<<1],size[MAXN<<1],q,ys[MAXN<<1],tmp;
int Next[4][2]={{0,1},{1,0},{0,-1},{-1,0}};
char a[MAXN];
//gf(),turn()略
void hb(int x,int y) {if(gf(x)!=gf(y)) {if(size[fa[y]]>size[fa[x]]) swap(x,y);size[fa[y]]+=size[fa[x]];fa[fa[x]]=fa[y];}}
int main()
{
n=read();m=read();tmp=n*m;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>a[turn(i,j)];
for(int i=1;i<=n*m;i++) {fa[i]=i;size[i]=((a[i]=='.')?1:0);}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
ys[turn(i,j)]=turn(i,j);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
if(i!=1)
{
if(a[turn(i,j)]=='.'&&a[turn(i-1,j)]=='.') hb(turn(i,j),turn(i-1,j));
}
if(j!=1)
{
if(a[turn(i,j)]=='.'&&a[turn(i,j-1)]=='.') hb(turn(i,j),turn(i,j-1));
}
}
q=read();
for(int i=1;i<=q;i++)
{
int op,w;
op=read();w=read();
if(op==1)
{
int flag=1,ans=0;
for(int j=1;j<=w;j++)
{
int x,y;
x=read();y=read();
if(a[turn(x,y)]=='.'&&size[gf(ys[turn(x,y)])]>ans)
{
ans=size[gf(ys[turn(x,y)])];
flag=j;
}
}
cout<<flag<<"\n";
}
else
{
for(int j=1;j<=w;j++)
{
int x,y;
x=read();y=read();
if(a[turn(x,y)]=='.')
{
a[turn(x,y)]='*';
size[gf(ys[turn(x,y)])]--;
}
else
{
ys[turn(x,y)]=++tmp;
a[turn(x,y)]='.';fa[ys[turn(x,y)]]=ys[turn(x,y)];size[ys[turn(x,y)]]=1;
for(int k=0;k<4;k++)
{
int tx=x+Next[k][0];
int ty=y+Next[k][1];
if(tx>0&&ty>0&&tx<=n&&ty<=m&&a[turn(tx,ty)]=='.') hb(ys[turn(x,y)],ys[turn(tx,ty)]);
}
}
}
}
}
return 0;
}
5.扩展域并查集&边带权并查集:
这道题我是用扩展域求解的,各位读者可以尝试使用边带权求解 (其实是我不会)
扩展域的原理:扩大并查集的上限来满足题目需要。
这道题,我们扩大并查集上线至 \(3*n\) ,由于不知道哪个动物在哪个组,令 \(1...n\) , \(n+1...2*n\) , \(2*n+1...3*n\)为三个组,\(x,x+n,x+2*n\) 表示同一个动物,如果是组内元素同祖先,表示他们是同类关系;如果是跨组同祖先,表示捕食关系,本题规定如果 \(gf(x)==gf(y+n)||gf(x+n)==gf(y+n+n)||gf(x+n+n)==gf(y)\) 那么 \(x\) 吃 \(y\) 。
如何判定一句话与前面的真话是矛盾的呢?
如果一句话告诉你 \(x,y\) 是同类,但是事实是 \(x\) 吃 \(y\) 或者 \(y\) 吃 \(x\) ,那么是假的,否则是真的,合并 \((x,y)\),\((x+n,y+n)\),\((x+n+n,y+n+n)\)。注意都要合并,否则传递不及时可能会导致一些错误。
如果一句话告诉你 \(x\) 吃 \(y\) ,但是事实是 \(x,y\) 是同类或者 \(y\) 吃 \(x\) ,那么是假的,否则是真的,合并 \((x,y+n)\),\((x+n,y+n+n)\),\((x+n+n,y)\)。
然后就做完了。如果实在看不懂我的题解,还可以看一看 luogu 题目里面的题解,或许能够更好的理解。
代码:
const int MAXN=5e4+10;
int n,k,fa[MAXN*3],ans=0;
int main()
{
n=read();k=read();
for(int i=1;i<=n*3;i++) fa[i]=i;
for(int i=1;i<=k;i++)
{
int op,x,y;
op=read();x=read();y=read();
if(x>n||y>n) ans++;
else if(op==2&&x==y) ans++;
else
{
if(op==1)
{
if(gf(x)==gf(y+n)||gf(x+n)==gf(y)) ans++;
else hb(x,y),hb(x+n,y+n),hb(x+n+n,y+n+n);
}
else
{
if(gf(x)==gf(y)||gf(y)==gf(x+n)) ans++;
else hb(x,y+n),hb(x+n,y+n+n),hb(x+n+n,y);
}
}
}
cout<<ans<<"\n";
return 0;
}
这道题使用边带权并查集来做。注意这一题的合并具有一定的方向性。
首先,我们令 \(front_i\) 表示 \(i\) 到根节点(领头羊)的距离,初始化为 0。\(num_i\) 表示以 \(i\) 为根节点的树的大小,初始化为 1。
然后,由于战队是一条链,但是我们路径压缩之后变成了一棵树,因此在路径压缩时先要加入这样一句话:
front[x]+=front[fa[x]]
保证 \(front\) 更新及时,然后才能路径压缩。这里又要注意,要先计算出 \(gf(fa[x])\) 并且存下之后才能更新,否则数据不够及时。
合并操作的时候,假设我们将 \(x\) 接到 \(y\) 后面,此时令 \(fx=gf(x),fy=gf(y)\) ,要让 \(fa_{fx}=num_{fy}\) ,因为此时此刻 \(x\) 不是祖先了,需要更新 \(front_{fx}\) ,不过不用着急将更新下传到孩子节点,因为路径压缩会帮你做好的qwq。
此时,由于 \(fy\) 后面加入了 \(num_{fx}\) 个节点,需要更新 \(num_{fy}+=num{fx}\) ,然后清零 \(num_{fx}\) 。
统计答案时,不在一个集合内输出 -1
,否则输出 \(|front_{x}-front_{y}|-1\) ,具体为什么请各位读者思考。
代码:
const int MAXN=30000+10;
int t,fa[MAXN],front[MAXN],num[MAXN];
int gf(int x)
{
if(fa[x]==x) return x;
int f=gf(fa[x]);
front[x]+=front[fa[x]];
return fa[x]=f;
}
int main()
{
t=read();
for(int i=1;i<=30000;i++) fa[i]=i,front[i]=0,num[i]=1;
for(int i=1;i<=t;i++)
{
char ch;int x,y;
cin>>ch;x=read();y=read();
if(ch=='M')
{
int fx=gf(x);
int fy=gf(y);
if(fx!=fy)
{
front[fx]=num[fy];
num[fy]+=num[fx];
num[fx]=0;
fa[fx]=fy;
}
}
else
{
if(gf(x)!=gf(y)) cout<<"-1\n";
else cout<<abs(front[x]-front[y])-1<<"\n";
}
}
return 0;
}
这道题两种做法都可以,不过个人认为扩展域并查集更好想也更好写。
将并查集容量扩大 2 倍,如果奇偶性相同则合并 \((x,y),(x+n,y+n)\),否则合并 \((x,y+n),(x+n,y)\) 。如果两个点已经在同一个集合内,仿照上例直接判断即可。
考虑到 \(n\) 很大,\(m\) 很小,需要先离散化每一个点。(不会离散化自行百度)
代码:
const int MAXN=1e5+10;
int n,m,a[MAXN],fa[MAXN],tmp,l[MAXN],r[MAXN],q[MAXN];
int main()
{
n=read();m=read();
for(int i=1;i<=m;i++)
{
string str;
l[i]=read();r[i]=read();cin>>str;
q[i]=(str=="odd")?1:0;
a[++tmp]=l[i]-1;a[++tmp]=r[i];//注意存的是l[i]-1,这里有前缀和的思想
}
sort(a+1,a+tmp+1);
n=unique(a+1,a+tmp+1)-a-1;//离散化
for(int i=0;i<=(n<<1);i++) fa[i]=i;
for(int i=1;i<=m;i++)
{
int x=lower_bound(a+1,a+n+1,l[i]-1)-a;
int y=lower_bound(a+1,a+n+1,r[i])-a;//找到离散化的点
//非C++选手请自行打二分,C++选手不懂得查百度
if(q[i]==1)
if(gf(x)==gf(y)||gf(x+n)==gf(y+n)) {cout<<i-1<<"\n";return 0;}
else hb(x,y+n),hb(x+n,y);
else
if(gf(x+n)==gf(y)||gf(x)==gf(y+n)) {cout<<i-1<<"\n";return 0;}
else hb(x,y),hb(x+n,y+n);
}
cout<<m<<"\n";
return 0;
}
4.总结
相信做完上述这 亿 一些例题后,各位都对并查集有了一定的了解。不过这些只是并查集的初等应用,并查集还有很多高级版本,比如可持久化并查集。这里不讲这些,太高深 且作者本人不会。并查集很多时候用于图论之中,或者是判断是否在同一个集合内。