并查集问题

目录

一、并查集的概念及其基本操作

1.书面定义

2.初始化

3.查询

4.合并

二、并查集的时空复杂度

时间复杂度

空间复杂度

三、模板题:亲戚

亲戚

四、点带权并查集

家族合并

 五、边带权并查集

堆箱子

六、种类并查集

敌人与朋友

题解

代码

食物链

 致谢


一、并查集的概念及其基本操作

1.书面定义

并查集是一种树形的数据结构。它用于处理一些不交集的 合并 及 查询 问题。查找:确定某个元素处于哪个子集;合并:将两个子集合并成一个集合。

这就是并查集的基本定义。相比这个,我觉得讲个故事更容易理解:

2.初始化

从前,有一个群人,他们分属于不同的家族,但他们一开始不知道自己属于哪个家族。找家族要先认祖宗,由于他们开始不知道自己的祖宗,因此他们自己作为自己的祖宗:

for(int i=1;i<=n;i++){
    f[i]=i;  //f[i]用来存储i的祖宗
}

3.查询

渐渐地,家族中有一些人相认了,他们渐渐找到了自己的一些先辈,除了最年长的祖先。为了确定自己处于哪个家族,同一个家族的人共同确定最年长的祖先作为祖宗。找最年长祖先的方式是找自己祖先的祖先,一直找下去,直到找到祖先为自己的人,即最年长的祖先为止:

int Find(int x){
    if(f[x]==x){
        return x;
    }
    return f[x]=Find(f[x]);
}

 如果两个人最年长的祖先是一个人,则两人处于同一个家族。

这就是并查集中的路径压缩,将每个子节点都连接到根节点上,大大提高了查询的效率。

4.合并

假设两个家族通婚,即两个家族要合并为一个家族,那么只需要分别找到两个家族的祖先,然后让其中一个认另一个为祖先即可:

void merge(int x,int y){
    int xx=Find(x),yy=Find(y);
    f[xx]=yy;
}

拓展:启发式合并 

又叫按秩合并,即在进行合并时,将深度小的集合合并到深度大的集合中,这样可以减少下次路径压缩的工作量。

这就是并查集概念与基本操作,下面我们来看一下它的时空复杂度:

二、并查集的时空复杂度

时间复杂度

同时使用路径压缩和启发式合并之后,并查集的每个操作平均时间仅为 O( α(n) ),其中 α(n) 为阿克曼函数的反函数,其增长极其缓慢,也就是说其单次操作的平均运行时间可以认为是一个很小的常数。这个常数绝大部分时候在8以下,可以直接将其时间复杂度看做O(1)。

空间复杂度

显然为O(n)。

三、模板题:亲戚

亲戚

时间限制:1秒        内存限制:128M

题目描述

若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
规定:x和y是亲戚,y和z是亲戚,那么x和z也是亲戚。如果x,y是亲戚,那么x的亲戚都是y的亲戚,y的亲戚也都是x的亲戚。

输入描述

第一行:三个整数n,m,p,(n<=5000,m<=5000,p<=5000),分别表示有n个人,m个亲戚关系,询问p对亲戚关系。
以下m行:每行两个数Mi,Mj,1<=Mi,Mj<=N,表示Mi和Mj具有亲戚关系。
接下来p行:每行两个数Pi,Pj,询问Pi和Pj是否具有亲戚关系。

输出描述

P行,每行一个’Yes’或’No’。表示第i个询问的答案为“具有”或“不具有”亲戚关系。

样例

输入

6 5 3
1 2
1 5
3 4
5 2
1 3
1 4
2 3
5 6

输出

Yes
Yes
No

提示

并查集

一道简单的模板题,直接使用并查集的基本操作即可。

#include<iostream>
using namespace std;
int n,m,q,f[5010];
int x,y;
int Find(int x){
    if(f[x]==x){
        return x;
    }
    return f[x]=Find(f[x]);
}
void merge(int x,int y){
    int xx=Find(x),yy=Find(y);
    f[xx]=yy;
}
int main(){
    cin>>n>>m>>q;
    for(int i=0;i<=n;i++){
        f[i]=i;
    }
    for(int i=1;i<=m;i++){
        cin>>x>>y;
        merge(x,y);
    }
    while(q--){
        cin>>x>>y;
        if(Find(x)==Find(y)){
            cout<<"Yes\n";
        }
        else{
            cout<<"No\n";
        }
    }
    return 0;
}

练习题我先只放一道,感兴趣的可以自行寻找题目。

四、点带权并查集

在并查集中,我们也可以记录一些值。例如,在上面“亲戚”这道题中,记录被查找者的亲戚数量。

我们通过下面这道题来讲解一下:

家族合并

时间限制:1秒        内存限制:128M

题目描述

有 n 个人,刚开始每个人都代表着一个家族,现在要对其进行操作,一共有如下三种操作:

1: C a b,a 和 b 所在的家族合并到一起

2:Q1 a b,查询 a 和 b 是否在同一个家族

3:Q2 a,查询 a 所在的家族有多少个人

输入描述

第一行输入整数 n 和 m。

接下来 m 行,每行包含一个操作指令,指令为 C a bQ1 a b 或 Q2 a 中的一种。

输出描述

对于每个询问指令 Q1 a b,如果 a 和 b 在同一个家族中,则输出 Yes,否则输出 No

对于每个询问指令 Q2 a,输出一个整数表示点 a 所在家族的人数

每个结果占一行。

输入样例

5 5

C 1 2

Q1 1 2

Q2 1

C 2 5

Q2 5

 

输出样例

Yes

2

3

数据描述

1≤n,m≤1e5
提示


输入输出较多,注意时间复杂度

在这道题中, C操作和Q1操作都是基本的合并与查找,那么Q2操作就需要用到点带权并查集的知识了。我们可以创建一个num数组,用来存储每个人的家族人数,即点权。num的使用:

x所在家族的人数:num[Find(x)];

合并时,如果f[x]=y,则y是x的父节点,要将x家族的人数加到y家族中,即num[y]+=num[x];

了解这些后,这道题就可以做出来了:

#include<iostream>
using namespace std;
int n,m;
int f[100010];
int num[100010];
string s;
int x,y;
int Find(int x){
    if(f[x]==x){
        return x;
    }
    return f[x]=Find(f[x]);
}
void merge(int x,int y){
    int a=Find(x),b=Find(y);
    if(a!=b){
        f[a]=b;
        num[b]+=num[a];
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    for(int i=1;i<=100010;i++){
        f[i]=i;
        num[i]=1;  //num数组的初始化,一定不要忘
    }
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        cin>>s;
        if(s=="C"){
            cin>>x>>y;
            merge(x,y);
        }
        else if(s=="Q1"){
            cin>>x>>y;
            if(Find(x)==Find(y)){
                cout<<"Yes\n";
            }
            else{
                cout<<"No\n";
            }
        }
        else{
            cin>>x;
            cout<<num[Find(x)]<<"\n";
        }
    }
    return 0;
}

 五、边带权并查集

除了上面记录同一个家族人数之外,我们还可以记录其他一些值,比如说x点到根节点的距离,即x是它最年长祖先的第几代后辈。记录的方式也很简单,在Find函数回溯时记录即可:

int dis[N];  //初始化为0
int Find(int x){
    if(f[x]==x){
        return x;
    }
    int root=Find(f[x]);  //得到根节点root
    dis[x]+=dis[f[x]];  //先更新dis数组
    return f[x]=root;  //再更新f数组
}

merge函数执行时dis数组根据题目要求更新即可。

堆箱子

时间限制:1秒        内存限制:128M

题目描述

小可有 n 个箱子,起初这 n 个箱子排成一排,每一个箱子单独放置,小可现在要开始堆箱子了,现在有两种操作:

M,x,y:将 x 所在的那一堆箱子,放置到 y 所在的那一堆箱子的上面,如果 x 和 y 本来就  在同一堆,则忽略这次操作

C x :询问 x 箱子的下面有多少个箱子

输入描述

第一行:输入两个数 n,m 表示有 n 个箱子,m 次操作

接下来 m 行:每行输入如题所述的两种操作之一

输出描述

对于每次查询操作,在单独一行输出一个答案

输入样例

6 6

M 1 6

C 1

M 2 4

M 2 6

C 3

C 4

输出样例

1

0

2

数据描述

1≤N≤30000,1≤m≤100000

这题我们可以让每一堆中最下面的箱子作为根节点,使用边带权并查集记录距离。

 需要注意的是dis数组在合并时的维护,我们可以用点带权并查集来记录每堆的箱子个数,再用num数组去更新dis数组。详见代码:

#include<iostream>
using namespace std;
int n,m;
char s;
int x,y;
int f[100010];
int num[100010];
int dis[100010];
int Find(int x){
    if(f[x]==x){
        return x;
    }
    int root=Find(f[x]);
    dis[x]+=dis[f[x]];
    return f[x]=root;
}
void merge(int x,int y){
    int xx=Find(x),yy=Find(y);
    if(xx!=yy){
        f[xx]=yy;
        dis[xx]=num[yy];  //用num去更新dis
        num[yy]+=num[xx];  //更新这一堆的数目
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=100010;i++){
        f[i]=i;
        num[i]=1;
        dis[i]=0;
    }
    for(int i=1;i<=m;i++){
        cin>>s;
        if(s=='M'){
            cin>>x>>y;
            merge(x,y);
        }
        else{
            cin>>x;
            Find(x);
            cout<<dis[x]<<"\n";
        }
    }
    return 0;
}

如果想要这题更详细的题解,请移步并查集——堆箱子题解_MZjtW的博客-CSDN博客

六、种类并查集

 并查集一般用于维护具有连通性的关系,如亲戚的亲戚是亲戚。但有时我们需要维护另一种关系:敌人的敌人是朋友。种类并查集就是用于解决这类问题的。我们可以想到,使用两个并查集可以用于分别存储敌人和朋友,但这样太过麻烦,我们可以将一个并查集扩大至原来的两倍。在有些问题中,我们甚至有可能遇到更多的关系,需要将并查集扩大3倍甚至更多,下面就会有这样的题目。

我们通过一道题来详细解释:

敌人与朋友

时间限制:1秒        内存限制:128M

题目描述

当我们有相同的敌人时,我们就是朋友,也就是说 “敌人的敌人是朋友”

基于这一点,小可做了一项研究:会不会存在两个人 “既是朋友又是敌人” 的关系,给出 n 个人,以及他们之间的 m 个敌对关系,请分析会不会存在上述关系

输入描述

多组输入:

第一行输入一个数 t ,表示样例组数

接下来的 t 组:

​ 每组的第一行:输入两个整数 n 和 m ,表示 n 个人,m 个敌对关系

​ 每组的接下来 m 行:每一行输入两个数字,a 和 b ,表示 a 和 b 是敌人关系

输出描述

对于每组样例:

如果存在 “既是朋友又是敌人” 的关系,输出 Yes,否则输出 No(在独立的一行输出)

输入样例

2

3 3

1 2

2 3

1 3

4 2

1 2

3 4

输出样例

Yes

No

数据描述

t≤20,1≤n≤2000,1≤m≤1000000
且总关系数不超过2000000

题解

题目中其实是有两种关系,即为两种状态,需要关系数组 f 开二倍,用 1~n 和 n+1~2n 分别表示两种状态,同一个集合表示是朋友,不同集合表示是敌人,对于一个人 x ,用 n+x 表示 x 的敌人。

题目给出的是敌对关系。所以在连接的时候:对于 x 和 y 是敌人关系, Union(x,y+n);   x 和 “y的敌人” 是朋友,merge(y,x+n);   y 和 “x的敌人” 是朋友。

判断是否存在“既是朋友又是敌人”的关系:对于给定的敌人关系合并之后,例如合并 x 和 y 之后。

出现了几种情况:

x 和 y 是朋友Find(x)==Find(y), x 和 x的敌人 是朋友Find(x)==Find(x+n), y 和 y的敌人 是朋友Find(y)==Find(y+n) 。

代码

#include<iostream>
#include<cstring>
using namespace std;
int n,m,t;
int flag;
int x,y;
int f[2000010];  //f数组开两倍
int Find(int x){
    if(f[x]==x){
        return x;
    }
    return f[x]=Find(f[x]);
}
void merge(int x,int y){
    int a=Find(x),b=Find(y);
    if(a!=b){
        f[a]=b;
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>t;
    while(t--){
        flag=0;
        cin>>n>>m;
        for(int i=1;i<=2*n;i++){  //初始化注意两倍
            f[i]=i;
        }
        for(int i=1;i<=m;i++){
            cin>>x>>y;
            merge(x,y+n);
            merge(y,x+n);
            if(Find(x)==Find(y)||Find(x)==Find(x+n)||Find(y)==Find(y+n)){
                flag=1;
            }
        }
        if(flag){
            cout<<"Yes\n";
        }
        else{
            cout<<"No\n";
        }
    }
    return 0;
}

食物链

时间限制:1秒        内存限制:128M

题目描述

动物王国中有三类动物 A,B,C,这三类动物的食物链构成了有趣的环形。A 吃 B,B吃 C,C 吃 A。
现有 N 个动物,以 1 - N 编号。每个动物都是 A,B,C 中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这 N 个动物所构成的食物链关系进行描述:
第一种说法是“1 X Y”,表示 X 和 Y 是同类。
第二种说法是“2 X Y”,表示 X 吃 Y 。
此人对 N 个动物,用上述两种说法,一句接一句地说出 K 句话,这 K 句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
• 当前的话与前面的某些真的话冲突,就是假话
• 当前的话中 X 或 Y 比 N 大,就是假话
• 当前的话表示 X 吃 X,就是假话
你的任务是根据给定的 N 和 K 句话,输出假话的总数。

输入描述

第一行两个整数,N,K,表示有 N 个动物,K 句话。
第二行开始每行一句话(按照题目要求,见样例)

输出描述

一行,一个整数,表示假话的总数。

样例

输入

100 7
1 101 1
2 1 2
2 2 3
2 3 3
1 1 3
2 3 1
1 5 5

输出

3

提示

1 ≤ N ≤ 5 ∗ 10^4
1 ≤ K ≤ 10^5

这题我不多做解释,大家看代码中的注释即可: 

#include<iostream>
#include<cstring>
using namespace std;
int n,k;
int a,x,y;
int ans;  //谎话的数量
int f[300010];  //1~n:同类  n+1~2n:猎物  2n+1~3n:天敌
int Find(int x){
    if(f[x]==x){
        return x;
    }
    return f[x]=Find(f[x]);
}
void merge(int x,int y){
    int a=Find(x),b=Find(y);
    if(a!=b){
        f[a]=b;
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>k;
    for(int i=1;i<=3*n;i++){
        f[i]=i;
    }
    for(int i=1;i<=k;i++){
        cin>>a>>x>>y;
        if(x>n||y>n){  //输入数字大于n:谎话
            ans++;
        }
        else{
            if(a==1){
                if(Find(x+n)==Find(y)||Find(x+2*n)==Find(y)){  //x和y不是同类,即x是y的猎物或天敌:谎话
                    ans++;
                }
                else{  //不是谎话才合并
                    merge(x,y);  //合并同类
                    merge(x+n,y+n);  //合并同类的猎物(同类的猎物是同类)
                    merge(x+2*n,y+2*n);  //合并同类的天敌(同类的天敌是同类)
                }
            }
            else{
                if(Find(x)==Find(y)||Find(x+2*n)==Find(y)){  //x不能吃y,即x和y是同类,或y是x的天敌
                    ans++;
                }
                else{  //不是谎话才合并
                    merge(x,y+2*n);  //x是y的天敌
                    merge(y,x+n);  //y是x的猎物
                    merge(x+2*n,y+n);  //x吃y,y吃z(y的猎物),则z吃x(y的猎物吃x)
                }
            }
        }
    }
    cout<<ans;
    return 0;
}

 致谢

以上就是全部内容,希望能对您有所帮助,感谢观看!

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值