并查集算法总结&专题训练

  • 1.概述
  • 2.模板
  • 3.例题
    • 1.入门题:
    • 2.与别的算法结合:
    • 3.考思维的题:
    • 4.二维转一维:
    • 5.扩展域并查集&边带权并查集:
  • 4.总结

1.概述

并查集是一种数据结构,用于图论之中(更多时候用于树),通常用来维护一张无向图内 O ( 1 ) O(1) O(1) 判断两个点是否在同一个连通块内,有很多的用法,扩展性也比较高。

2.模板

下面还是通过一道模板讲解并查集的用法。

link

我们假设这 4 个元素分别表示 4 个人。假设每个人都会在一个群内,第 i i i 个人的群主表示为 f a i fa_i fai​ (其实如果抽象成一棵树,就是 i i i 的父亲节点)

初始时,每一个人单独在一个群内,则令 f a i = i fa_i=i fai​=i (这一步是并查集的初始化,非常重要,否则所有人的群主/每个点的祖先就都变成了不存在的 0 号节点,后续操作就会出现很多奇奇怪怪的错误)

看操作 1 :问第 1 个人与第 2 个人在不在同一个群内。

显然不在了,大家各管各的,输出 N

操作 2:合并第 1 个人与第 2 个人所在的群。

如何合并呢?此时两个人分属于不同的群,现在要将两个人合成一个群,那么我们直接把 2 的群主改成 1 不就可以了?即令 f a 2 = 1 fa_2=1 fa2​=1 。从树的角度看,初始时每一个点都是单独的根节点,现在在 1 和 2 之间连一条边,生成一棵新树,同时令 1 为根节点。

接下来又问 1 与 2 在不在一个群内。

这一步是并查集的判断操作,判断时我们发现, f a 1 = f a 2 fa_1=fa_2 fa1​=fa2​ ,那么他们在一个群内,输出 Y

此时群组情况如下所示:

1    3    4 \2

接下来合并 3 4.仿照上述步骤,令 f a 4 = 3 fa_4=3 fa4​=3 。

这里说明一下,其实令 f a 3 = 4 fa_3=4 fa3​=4 也是可以的,看个人习惯,本质上并没有什么区别,毕竟都在一个群里面,谁是群主都没有问题。

群组情况 :

1     3\     \2     4

下一个操作询问 1 4 在不在一个群内, f a 1 = 1 fa_1=1 fa1​=1, f a 3 = 3 fa_3=3 fa3​=3 ,群主不一样,不在一个群内,输出 N

下一步合并 2 3,此时。。。。。。

1 不同意了!如果我们修改 f a 3 = 2 fa_3=2 fa3​=2 ,没有什么问题(具体为什么见下文),但是万一程序让 f a 2 = 3 fa_2=3 fa2​=3 ,那么 1 就不同意了:“ 2 明明在我的群,凭什么到你的群去了?”怎么办?

既然 2 搞不定,我们直接找最高群主 1 谈谈,直接令 f a 1 = 3 fa_1=3 fa1​=3 就可以解决了。从树的角度看,就是改变根节点的父亲。

群主情况:

     3/ \1   4\2

接下来问 1 4 在不在一个群内, f a 1 = f a 4 fa_1=fa_4 fa1​=fa4​ ,在一个群内,输出 Y

然而此时,如果再来一个询问:询问 2 4在不在一个群内,要怎么办呢?

肉眼可见, 2 4 在一个群内,应该输出 Y ,然而我们上面的判断都是根据 f a i fa_i fai​ 是否相等判断的,此时并不相等,不就出问题了吗?

为了解决这个问题,方法是:找到最高群主也就是根节点。

看图,2 的群主是 1 ,而 1 的群主是 3 ,这样 2 的最高群主不就是 3 了吗?4 的最高群主也是 3 ,在同一个群内。

如果此时又来一个问题:判断现在有几个群要怎么办呢?

由于每一个群都有最高群主,且只有最高群主的群主是自己(为什么?),那么只要统计出有几个 i ∈ [ 1 , n ] i \in [1,n] i∈[1,n] 使得 f a i = = i fa_i==i fai​==i 即可。也就是找出每一棵树的根节点。

完美解决~~~

代码实现:

  1. 初始化:
    这里直接一遍 for 即可。

    for(int i=1;i<=n;i++) fa[i]=i;
    
  2. 查找某个节点的最高群主也就是根节点。
    递归查找即可,代码如下:

    int gf(int x) {return (fa[x]==x)?x:gf(fa[x]);}
    

    其中, f a x = = x fa_x==x fax​==x 表示找到根节点了(根节点的父亲就是根节点,初始化时已经这样操作过了),找到返回 x x x ,否则递归查找。
    然而你以为这样就结束了吗?看下图:

    1-2-3-4-5-6-7-......-op(某极大的数字,比如说 1e5 1e6 之类的)
    

    这样,查询一次 f a o p fa_{op} faop​ 就要 O ( 1 e 5 或 1 e 6 ) O(1e5或1e6) O(1e5或1e6) 的时间复杂度,多查几次不就 TLE 了?为了解决这个问题,我们引入一个优化:路径压缩。
    路径压缩的目的就是为了解决上面的问题,即在查找某节点的祖先的时候,我们将一路上查找的所有节点的父亲全部连到根节点,也就是变成下图:

    1
    | \ \ \   \
    2  3 4 ... op
    

    这样,查询复杂度直接降至 O ( 1 ) O(1) O(1) ,大大优化查询复杂度。
    而代码只需要这样改:

    int gf(int x) {return (fa[x]==x)?x:fa[x]=gf(fa[x])}
    
  3. 合并操作:
    找到根节点合并即可。

    void hb(int x,int y) {if(gf(x)!=gf(y)) fa[fa[x]]=fa[y];}
    //由于加入了路径压缩所以不会有问题。
    
  4. 查询操作
    判断两个点的祖先是否相同即可。

    cout<<((gf(x)==gf(y))?'Y':'N')<<"\n";
    //实测这里三目运算符外面不加括号会CE
    
  5. 统计树的数量
    根据上述所讲,一遍 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.入门题:

[BOI2003]团伙

这道题是一道练手题,思维与算法难度都不高,就是一个并查集。

首先处理读入数据,将是朋友的人合并,是敌人的人先存在 v v 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 c_i,d_i ci​,di​ 全部加起来,放到新数组 m o n e y j , v a l u e j money_j,value_j moneyj​,valuej​ 中,跑一遍 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 d_i di​ 表示 i i i 的第一个会与他发生摩擦的人,初始化为 0 。

接下来处理数据。假设此时我们要处理 a a a 与 b b b 发生摩擦,怒气值为 c c c 的信息:

  1. 如果此时 a , b a,b a,b 已经在一起了,直接输出 c c c ,结束程序。
  2. 否则,他们不在一起,以 a a a 为例:如果 d a = 0 d_a=0 da​=0,说明此时没有人与他有摩擦,则 d a = b d_a=b da​=b ,否则说明已经有人与他有摩擦了,由于只有两个监狱,那么合并 b , d a b,d_a b,da​ 即可。
  3. 正确性:显然要将 d a , a d_a,a da​,a 拆掉。假设 b , d a b,d_a b,da​ 之间的怒气值(如果没有摩擦为 0)为 c ′ c' c′ ,根据之前的排序,必然有 c ′ < c c'<c c′<c,那么显然合并 b , d a b,d_a b,da​ 比合并 a , b a,b 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.考思维的题:

[JSOI2008]星球大战

正常的并查集支持合并操作,但是不支持删除操作,然而这道题的所有操作不是合并就是删除,那么要怎么办呢?

既然并查集支持合并操作,那么我们想办法支持合并操作就好了呗!

我们将打击星球的顺序 倒过来操作 ,将其视作 重建星球 ,然后每重建一个合并一次,最后处理连通块个数不就好了qwq。

关于如何处理连通块个数,这里提供一个 O ( 1 ) O(1) O(1) 的思想:

  1. 初始化 s u m = n sum=n sum=n ,表示有 n n n 个连通块。
  2. 每合并两个点, s u m − − sum-- sum−− 。
  3. 注意合并的两个点不能在同一个连通块内。

注意:最后输出答案时要逆序输出,没有重建的星球不算在答案内,只有当两个星球全部重建完成才能合并。这里我使用 b o o k book 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;
}

[IOI2014]game 游戏

别看这道题是 IOI 的题目,其实想通了真的非常简单。你看评级都是绿色

首先,为了让梅玉只有到最后一个询问才能判断是否连通,这里就有一种思路:我们构造某一张图使得这张图连通,且最后一个询问问的点 x , y x,y x,y 会连一条边,这里记作 x − > y x->y x−>y ,但是一旦我们删去了 x − > y x->y x−>y 整张图就会裂成两个集合,换句话说, x − > y x->y x−>y 是这一张图的桥/割边。(桥/割边的定义:如果删除某条 u − > v u->v u−>v 的边后途中连通块个数增加,那么 x − > y x->y x−>y 是这张图的桥/割边)

为什么正确呢?如果 x − > y x->y x−>y 是这张图的割边,那么在倒数第二个询问中梅玉依然不能判断整张图是否连通(有 2 个连通块),此时她必须再问一次才能确定图是否连通。

因此思路就很明确了,我们对于某个询问 a − > b a->b a−>b ,如果合并 a , b a,b a,b 以后 x , y x,y x,y 在一个连通块内,这显然不是我们想要的操作,此时不能合并 a , b a,b a,b ;否则,合并 a , b a,b a,b 。最后不要忘记输出最后一条边是否连通,这里输出 01 都可以。不过根据我们构造的方案,最好输出 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,j) (第 i i i 行第 j j j 列,下同),我们将其在一维编号为 ( i − 1 ) ∗ m + j (i-1)*m+j (i−1)∗m+j (注意不是 n n n),然后对于相邻的两个二维的点连一条边,将点压成一维后按照边权从小到大排序。

然后,对于每一条边:

  1. 首先,如果这条边连着的两个点在一个连通块内, continue;
  2. 然后,如果两棵子树 s i z e size size 和大于等于 t t t,那么 a n s + = c ∗ ( c n t i ) ans+=c*(cnt_i) ans+=c∗(cnti​)( c n t i cnt_i cnti​ 见下文)。 其中, i i i 为两个子树的编号,且 s i z e i < t size_i<t sizei​<t 。为什么大于 t t t 的就不能统计了呢?因为之前 s i z e i < t size_i<t sizei​<t 的时候已经统计过一次,此时又统计就会造成浪费,并且即使有新的起点加入,也已经被统计过,没有意义了。
  3. 而后合并两个点,如果这两个点内有起点,我们就将增加的起点个数存在 c n t i cnt_i cnti​ 里面。

代码:

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;
}

小 D 的地下温泉

这道题类似,首先二维转一维不说,然后如果两个相邻点都是泉水合并。

询问操作:直接求出询问点所在树的 s i z e size size 即可,求个最大值。

有个坑点:当心所有点都是土地,此时我们需要输出 1

修改操作:泉水改土地直接修改地图然后 s i z e − − size-- size−− 即可。土地改泉水时我们需要新开一个点,改变地图后令新开的点 f a = 自 己 , s i z e = 1 fa=自己,size=1 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.扩展域并查集&边带权并查集:

[NOI2001]食物链

这道题我是用扩展域求解的,各位读者可以尝试使用边带权求解 (其实是我不会)

扩展域的原理:扩大并查集的上限来满足题目需要。

这道题,我们扩大并查集上线至 3 ∗ n 3*n 3∗n ,由于不知道哪个动物在哪个组,令 1... n 1...n 1...n , n + 1...2 ∗ n n+1...2*n n+1...2∗n , 2 ∗ n + 1...3 ∗ n 2*n+1...3*n 2∗n+1...3∗n为三个组, x , x + n , x + 2 ∗ n x,x+n,x+2*n x,x+n,x+2∗n 表示同一个动物,如果是组内元素同祖先,表示他们是同类关系;如果是跨组同祖先,表示捕食关系,本题规定如果 g f ( x ) = = g f ( y + n ) ∣ ∣ g f ( x + n ) = = g f ( y + n + n ) ∣ ∣ g f ( x + n + n ) = = g f ( y ) gf(x)==gf(y+n)||gf(x+n)==gf(y+n+n)||gf(x+n+n)==gf(y) gf(x)==gf(y+n)∣∣gf(x+n)==gf(y+n+n)∣∣gf(x+n+n)==gf(y) 那么 x x x 吃 y y y 。

如何判定一句话与前面的真话是矛盾的呢?

如果一句话告诉你 x , y x,y x,y 是同类,但是事实是 x x x 吃 y y y 或者 y y y 吃 x x x ,那么是假的,否则是真的,合并 ( x , y ) (x,y) (x,y), ( x + n , y + n ) (x+n,y+n) (x+n,y+n), ( x + n + n , y + n + n ) (x+n+n,y+n+n) (x+n+n,y+n+n)。注意都要合并,否则传递不及时可能会导致一些错误。

如果一句话告诉你 x x x 吃 y y y ,但是事实是 x , y x,y x,y 是同类或者 y y y 吃 x x x ,那么是假的,否则是真的,合并 ( x , y + n ) (x,y+n) (x,y+n), ( x + n , y + n + n ) (x+n,y+n+n) (x+n,y+n+n), ( x + n + n , y ) (x+n+n,y) (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;
}

[NOI2002]银河英雄传说

这道题使用边带权并查集来做。注意这一题的合并具有一定的方向性。

首先,我们令 f r o n t i front_i fronti​ 表示 i i i 到根节点(领头羊)的距离,初始化为 0。 n u m i num_i numi​ 表示以 i i i 为根节点的树的大小,初始化为 1。

然后,由于战队是一条链,但是我们路径压缩之后变成了一棵树,因此在路径压缩时先要加入这样一句话:

front[x]+=front[fa[x]]

保证 f r o n t front front 更新及时,然后才能路径压缩。这里又要注意,要先计算出 g f ( f a [ x ] ) gf(fa[x]) gf(fa[x]) 并且存下之后才能更新,否则数据不够及时。

合并操作的时候,假设我们将 x x x 接到 y y y 后面,此时令 f x = g f ( x ) , f y = g f ( y ) fx=gf(x),fy=gf(y) fx=gf(x),fy=gf(y) ,要让 f a f x = n u m f y fa_{fx}=num_{fy} fafx​=numfy​ ,因为此时此刻 x x x 不是祖先了,需要更新 f r o n t f x front_{fx} frontfx​ ,不过不用着急将更新下传到孩子节点,因为路径压缩会帮你做好的qwq。

此时,由于 f y fy fy 后面加入了 n u m f x num_{fx} numfx​ 个节点,需要更新 n u m f y + = n u m f x num_{fy}+=num{fx} numfy​+=numfx ,然后清零 n u m f x num_{fx} numfx​ 。

统计答案时,不在一个集合内输出 -1 ,否则输出 ∣ f r o n t x − f r o n t y ∣ − 1 |front_{x}-front_{y}|-1 ∣frontx​−fronty​∣−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;
}

[CEOI1999]Parity Game

这道题两种做法都可以,不过个人认为扩展域并查集更好想也更好写。

将并查集容量扩大 2 倍,如果奇偶性相同则合并 ( x , y ) , ( x + n , y + n ) (x,y),(x+n,y+n) (x,y),(x+n,y+n),否则合并 ( x , y + n ) , ( x + n , y ) (x,y+n),(x+n,y) (x,y+n),(x+n,y) 。如果两个点已经在同一个集合内,仿照上例直接判断即可。

考虑到 n n n 很大, m m 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);elseif(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.总结

相信做完上述这 亿 一些例题后,各位都对并查集有了一定的了解。不过这些只是并查集的初等应用,并查集还有很多高级版本,比如可持久化并查集。这里不讲这些,太高深 且作者本人不会。并查集很多时候用于图论之中,或者是判断是否在同一个集合内。

并查集算法总结专题训练相关推荐

  1. 并查集入门+初级专题训练

    介绍   摘自罗勇军,郭卫斌的<算法竞赛入门到进阶>上的说明:   并查集(Disjoint Set)是一种非常精巧而且食用的数据结构,它主要用于处理一些不相交集合的合并问题.经典的例子有 ...

  2. 给我三分钟,带你领略热血江湖中的并查集算法

    你好,我是小黄,一名独角兽企业的Java开发工程师. 校招收获数十个offer,年薪均20W~40W. 感谢茫茫人海中我们能够相遇, 俗话说:当你的才华和能力,不足以支撑你的梦想的时候,请静下心来学习 ...

  3. Union-Find 并查集算法详解

    Union-Find 并查集算法详解 文章目录 Union-Find 并查集算法详解 一.问题介绍 二.基本思路 三.平衡性优化 四.路径压缩 五.总结 六.例题 一.问题介绍 简单说,动态连通性其实 ...

  4. 简单易懂的并查集算法以及并查集实战演练

    文章目录 前言 一.引例 二.结合引例写出并查集 1. 并查集维护一个数组 2. 并查集的 并 操作 3. 并查集的 查 操作 4. 基本并查集模板代码实现--第一版(有错误后面分析) 4.1 Jav ...

  5. C++并查集算法(详细)

    C++并查集算法 什么是并查集? 并查集写法 详解 例题:洛谷 P3367.[模板]并查集 题意 代码 什么是并查集? 当我们在做图论题目的时候 经常会读到一些长这样的题目描述: -连接 a , b ...

  6. 并查集算法 | Union-Find Algorithm

    Union-Find Algorithm即并查集算法,常用于解决 动态连通性,判断有向无圈图等问题. 根本上讲,Union-Find算法就和他的名字一样是一种对不相交集数据结构执行两个有用操作的算法, ...

  7. 并查集算法----犯罪团伙(黑科技)

    一.题目描述 犯罪团伙(gang.cpp) 题目描述  警察抓到了n个罪犯,警察根据经验知道他们属于不同的犯罪团伙,却不能判断有多少个团伙,但通过警察的审讯,知道其中的一些罪犯之间相互认识,已知同一犯 ...

  8. 2021年SWPUACM暑假集训day2并查集算法

    什么是并查集 并查集是一种树形的数据结构,顾名思义,它用于处理一些不交集的 合并 及 查询 问题. 它支持两种操作: 1.查找(find):确定某个元素处于哪个子集 2.合并(merge):将两个子集 ...

  9. java---并查集算法_食物链(每日一道算法2022.8.17)

    难度警告!今天的题思路比较复杂,涉及数学知识congruence class 每天一道算法居然已经一个月了啊,期间居然没断更哈哈 呼呼~算法基础课过去三分之一了,啊后面好像越来越难了呜呜呜,还能保持日 ...

最新文章

  1. 缓存方式之cookie的使用
  2. python爬取b站粉丝数_【python爬虫】每天统计一遍up主粉丝数!
  3. mysql实现日志系统_基于Hadoop/CloudBase/MySQL的日志分析系统的设计与实现
  4. python数据分析知识点_Python基础知识点总结:数据
  5. 虹桥地铁站附近沿线的有房源出租的社区和村落
  6. linux cpu使用率1200%,linux下用top命令查看cpu利用率超过100%
  7. 配置oracle网络连接命令,配置oracle网络环境
  8. 仿淘宝分页按钮效果简单美观易使用的JS分页控件
  9. python面向对象编程第2版_python面向对象编程(2),之,二
  10. 每次连接服务器都要source ~/.bashrc问题
  11. wmv格式转html格式转换器,iPixSoft SWF to HTML5 Converter
  12. 新浪UC聊天室的几个漏洞
  13. SEO 优化--助力网站推广
  14. ad引脚名字设置_AD软件管脚名称如何放置负信号?
  15. 画坦克__线程__V1__第一种方法创造线程
  16. 19c 单实例打补丁
  17. android陌陌权限申请实现,Hook实现Android 微信,陌陌 ,探探位置模拟
  18. 熹妃传服务器维护10.16,熹妃传2016最新版厨艺大赛攻略全解
  19. python根据excel生成报表_Python实现导出数据生成excel报表的方法示例
  20. 巴菲特和西蒙斯谁的投资更赚钱?股神巴菲特的投资理念是否好用?

热门文章

  1. Web Workers 入门学习
  2. 论文阅读-Federated Social Recommendation with Graph NeuralNetwork
  3. 4种方法解除ZIP压缩文件的密码保护
  4. 苹果手机查看python代码_[代码全屏查看]-基于Python的苹果序列号官网查询接口调用代码实例...
  5. 什么是 RTMP拉流,如何使用它来提高你的直播质量
  6. FlashFXP 注册码
  7. Python决策树、随机森林、朴素贝叶斯、KNN(K-最近邻居)分类分析银行拉新活动挖掘潜在贷款客户
  8. java replaceall 引号_Java 1.4 String.replaceAll单引号问题
  9. eclipse各个版本简介
  10. 约数大合集(超详细!!!)