并查集详解(点权、边权、种类)

题目引入

亲戚
题目背景
若某个家族人员过于庞大,要判断两个是否是亲戚,确实很不容易,现在给出某个亲戚关系
图,求任意给出的两个人是否具有亲戚关系。
题目描述
规定: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
6 4
5 2
1 3
1 4
2 3
5 6
样例输出
Yes
Yes
No

把这个问题抽象成集合,每个有亲戚关系的人组成一个帮派,然后有亲戚关系的人合并成一家,还有查询两人是否有亲戚关系。往往遇见这种集合合并查询问题,直接用并查集

并查集是一种树型数据结构,擅长处理集合合并为不相交集后的查询问题 (该数据结构用普通数组即可实现)

基本步骤

  1. 自封为父(初始化):字面意思,自己就是自己的爹
  2. 寻亲:只要发现有人跟自己有亲戚关系,就叫爸爸(bushi
  3. 查询自己的祖先:某天,儿子心血来潮,问他的老爸:“敢问祖先性甚名谁?” 老爸忽然懵了,暗自想到“我以前怎么没有想过这个问题”,……,直到问道一个没爹的,说“老子就是恁爹!!!”,然后再一路传下去

考虑一个极端情况,这一个家族亲戚关系排成了一个链,那查询一次的时间复杂度趋近与O(n)。所以,我们要进行一个叫做 路径压缩 的东西,就是把所有人的辈分都磨平,一个家族的人都拥有一个共同的并且只有一个的“爹”,抽象成树形结构就是一棵树只有一个根节点,高度为2,剩下全是叶子结点

然后,并查集就趋近于常数级,≈O(4)(下面的了解一下,反正我是看不懂)
1975 年 Tarjan 证明,若将路径压缩与按秩归并合联合使用,n 次操作需要花费 O(n·α(n))
时间。α(n)是单变量阿克曼函数的逆函数,它是一个增长速度比 logn 慢的多但又不是常数
的函数,在一般情况下α(n)≤4,可以当作常数看

#include
#include
#include
#include
#include
#include
#include
#include
#define TIE ios::sync_with_stdio(false), cin.tie(0), cout.tie(0)
#define N 5010
#define INF 0x3f3f3f3f
#define ll long long
using namespace std;
int father[N],n,m,p;
int Find(int x){
    if(x==father[x]) return x;
    return father[x]=Find(father[x]);
}
int main() {
    TIE;cin>>n>>m>>p;
    for(int i=1;i<=n;++i) father[i]=i;
    for(int i=1;i<=m;++i){
        int x,y;
        cin>>x>>y;
        father[Find(x)]=Find(y);
    }
    while(p--){
        int x,y;
        cin>>x>>y;
        if(Find(x)==Find(y)) cout<<"Yes"<<"\n";
        else cout<<"No"<<"\n";
    }
    return 0;
}

本题的代码就是并查集最基本的模板~~~

题目再引入

信息传递

题目描述
有 n 个同学(编号为 1 到 n )正在玩一个信息传递的游戏。在游戏里每人都有一个固定
的信息传递对象,其中,编号为 i 的同学的信息传递对象是编号为 Ti 的同学。
游戏开始时,每人都只知道自己的生日。之后每一轮中,所有人会同时将自己当前所知的生
日信息告诉各自的信息传递对象(注意:可能有人可以从若干人那里获取信息, 但是每人
只会把信息告诉一个人,即自己的信息传递对象)。当有人从别人口中得知自 己的生日时,
游戏结束。请问该游戏一共可以进行几轮?
输入描述
共 2 行。
第 1 行包含 1 个正整数 n,表示 n 个人。
第 2 行包含 n 个用空格隔开的正整数 T1,T2,⋯⋯,Tn,
其中第 i 个整数 Ti 表示编号为 i 的同学的信息传递对象是编号为 Ti 的同学,
Ti≤n 且 Ti≠i。
输出描述
1 个整数,表示游戏一共可以进行多少轮。
对于 30%的数据, n ≤ 200;
对于 60%的数据,n ≤ 2500;
对于 100%的数据,n ≤ 200000。
样例输入
5
2 4 2 3 1
样例输出
3

本题其实就是求解给定的图中所有环中的最小环的边数,但怎么求呢?
提供一种思路:我们维护一个每个点到代表元的距离的数组

为了保持并查集的优秀复杂度,我们当然得在Find()函数和Union()函数上下手

观察 Find()函数的路径压缩:
从 x 出发向 root 递归的过程中,我们保存了 x 到 root 的所有沿途节点。
在 root 向 x 回溯的过程中,我们更新了所有的 father 值。
同样地,在这个过程中,我们可同时更新沿途节点到 root 的距离

所以我们的函数们变成了这样:

int Find(int x){
    if(x==father[x]) return x;
    int root=Find(father[x]);//记得这里是找它的父亲
    dis[x]+=dis[father[x]];
    return father[x]=root;
}
void Union(int x,int y){
    int xx=Find(x);//这里因为只是比较两个元素的代表元是否相同,原点因为还得求值,
    int yy=Find(y);//所以还得保留,不能覆盖
    if(xx==yy) ans=min(ans,dis[x]+dis[y]+1);//这个是此题的骚操作,不是模板!!!
    father[xx]=yy;//这里也是
    dis[x]=dis[y]+1;
}

于是乎,带点上面这种骚操作的并查集就是边带权并查集

先打住,以读者你们这些dalao的联想能力,应该能想到,有边带权,那必定有点带权呀!!!

家族合并

题目描述
有 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≤105
​​

这个题目就是简单的并查集模板加上了一个操作:记录这个集合中的所有元素,很简单,还是把Find()和Union()改一下就行了

#include
#include
#include
#include
#include
#include
#include
#include
#define TIE ios::sync_with_stdio(false), cin.tie(0), cout.tie(0)
#define N 100010
#define INF 0x3f3f3f3f
#define ll long long
using namespace std;
int father[N],n,m,p,num[N];
int Find(int x){
    if(x==father[x]) return x;
    return father[x]=Find(father[x]);
}
void Union(int x,int y){
    x=Find(x);
    y=Find(y);
    father[x]=y;
    num[y]+=num[x];
}
int main() {
    TIE;cin>>n>>m;
    for(int i=1;i<=n;++i){
        father[i]=i;
        num[i]=1;
    }
    for(int i=1;i<=m;++i){
        int x,y;
        string re;
        cin>>re;
        if(re=="C"){
            cin>>x>>y;
            if(Find(x)!=Find(y)) Union(x,y);
        }
        else if(re=="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吃y就把x的天敌和y的猎物设为同类,x的猎物和y设为同类,x和y的天敌设为同类
#include
#include
#include
#include
#include
#include
#include
#include
#define TIE ios::sync_with_stdio(false), cin.tie(0), cout.tie(0)
#define N 150010
#define INF 0x3f3f3f3f
#define ll long long
using namespace std;
int n,k,f[N],ans;
int Find(int x){
    if(x==f[x]) return x;
    return f[x]=Find(f[x]);
}
void Union(int x,int y){
    int xx=Find(x),yy=Find(y);
    f[xx]=yy;
}
int main() {
    TIE;cin>>n>>k;
    for(int i=1;i<=3*n;++i) f[i]=i;
    for(int i=1;i<=k;++i){
        int a,x,y;
        cin>>a>>x>>y;
        if(x>n||y>n){ans++;continue;}
        if(a==1){
            if(Find(x+n)==Find(y)||Find(x+2*n)==Find(y)){ans++;continue;}
            Union(x,y),Union(x+n,y+n),Union(x+2*n,y+2*n);
        }else{
            if(Find(x)==Find(y)||Find(x+2*n)==Find(y)){ans++;continue;}
            Union(x+n,y),Union(x,y+2*n),Union(x+2*n,y+n);
        }
    }
    cout<<ans<<"\n";
    return 0;
}

你可能感兴趣的:(c++,算法)