目录
一、并查集的概念及其基本操作
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 b
,Q1 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;
}
致谢
以上就是全部内容,希望能对您有所帮助,感谢观看!