并查集
并查集结构能够支持快速进行如下的操作:
- 将两个集合合并;
- 询问两个元素是否在一个集合当中
并查集可以在近乎 O ( 1 ) O(1) O(1)的时间复杂度下吗,完成上述2个操作
基本原理 用树的形式来维护一个集合。用树的根节点来代表这个集合。对于树中的每个节点,都用一个数组存储其父节点的编号。比如一个节点为x,用p[x]来表示其父节点的编号。当我们想求某一个节点所属的集合时,找到其父节点,并一直往上找,直到找到根节点,下面考虑几个问题:
- 如何才能判断根节点?
当父节点为其本身时,此节点即为根节点:p[x] == x; - 如何求某节点x的集合编号?
从当前父节点不断向上遍历,直到找到根节点,即为集合编号:while(p[x]!=x) x=p[x]; - 如何合并两个集合?
令某个集合的根节点为另一个集合根节点的父节点。
优化:
未优化前查找某个元素的所属集合(也即查找两个元素是否在一个集合)的时间复杂度为
O
(
l
o
g
n
)
O(log n)
O(logn)即树的高度,为使时间复杂度接近
O
(
1
)
O(1)
O(1),采用路径压缩的方法优化:即每次查找元素所属集合的过程中,顺便使每个节点的父节点都指向根节点(即集合编号节点)
查询根节点:find(x);
合并两个结合:p[find(a)] = find(b);
判断两个集合是否在同一个集合:if(find(a) == find(b);
下面先给出朴素并查集的板子:
int n, m;
int p[N]; // p[i] 表示元素 i 的父节点
// 返回 x 所在集合的编号(祖宗节点) + 路径压缩
int find(int x) {
if (p[x] != x) // 如果 x 不是自身的父节点,说明还没有找到根节点
p[x] = find(p[x]); // 递归查找 x 的父节点,并进行路径压缩
return p[x]; // 返回根节点
}
//初始化,假定节点编号为1~n
for(int i = 1; i <= n; i ++) p[i] = i;
//合并a,b所在的两个集合
p[find(a)] = find(b);
Acwing 836.合并集合
一共有 n个数,编号是 1∼n ,最开始每个数各自在一个集合中。
现在要进行 m个操作,操作共有两种:
M a b,将编号为 a和 b的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
Q a b,询问编号为 a和 b的两个数是否在同一个集合中;
输入样例:
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出样例:
Yes
No
Yes
具体实现代码(详细注释版):
#include <iostream>
#include <vector>
using namespace std;
const int N = 100010;
int n, m;
int p[N]; // p[i] 表示元素 i 的父节点
// 返回 x 所在集合的编号(祖宗节点) + 路径压缩
int find(int x) {
if (p[x] != x) // 如果 x 不是自身的父节点,说明还没有找到根节点
p[x] = find(p[x]); // 递归查找 x 的父节点,并进行路径压缩
return p[x]; // 返回根节点
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) p[i] = i; // 初始化,每个元素的父节点是它自己
while (m--) {
char op;
int a, b; // 操作涉及的两个元素 a 和 b
cin >> op >> a >> b;
if (op == 'M') // 合并操作
p[find(a)] = find(b); // 将 a 的根节点连接到 b 的根节点
else { // 查询操作
if (find(a) == find(b)) // 如果 a 和 b 的根节点相同
puts("Yes"); // 输出 Yes
else
puts("No"); // 否则输出 No
}
}
return 0;
}
这就是并查集最基本的操作:合并和查询。当然,还有其它比如集合中元素个数的问题。
Acwing837 .连通块中点的数量
给定一个包含 n个点(编号为 1∼n)的无向图,初始时图中没有边。
现在要进行 m 个操作,操作共有三种:
C a b,在点 a 和点 b之间连一条边,a和 b 可能相等;
Q1 a b,询问点 a和点 b 是否在同一个连通块中,a 和 b 可能相等;
Q2 a,询问点 a所在连通块中点的数量;
输入样例:
5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5
输出样例:
Yes
2
3
实现思路:一个连通块视为一个集合,相比朴素并查集哦,就是多了一个size大小需要维护
- 在a和b之间连一条边,就是合并两个集合,若属于同一个集合则无需操作;
- 询问a和b是否在一个连通块,即询问a,b是否在同一个集合内;
- 询问a所在连通块的数量,即每次合并需额外维护一个数组记录集合中的元素个数
下面先给出维护size的并查集的板子:
int p[N],sizee[N];
//p[]存储每个点的祖宗节点,size[]只有祖宗节点的有意义,表示祖宗节点所在集合中点的数量
// 返回 x 所在集合的编号(祖宗节点) + 路径压缩
int find(int x) {
if (p[x] != x) // 如果 x 不是自身的父节点,说明还没有找到根节点
p[x] = find(p[x]); // 递归查找 x 的父节点,并进行路径压缩
return p[x]; // 返回根节点
}
//初始化,假定节点编号为1~n
for(int i = 1; i <= n; i ++){
p[i] = i;
size[i] = 1;
}
//合并a,b所在的两个集合
size[find(b)] += size[find(a)];
p[find(a)] = find(b);
具体实现:
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 1e6+10;
int n,m;
int p[N];
int sizee[N];//记录对应集合中的元素个数,只有集合根节点对应记录有意义
int find(int x){
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main(){
scanf("%d%d",&n,&m);
for(int i = 1; i <= n; i++){
p[i] = i;
sizee[i] = 1;//刚开始都为1
}
while(m --){
char op[5];
int a,b;
scanf("%s",op);
if(op[0] == 'C'){
scanf("%d%d",&a,&b);
if(find(a) == find(b)) continue;// 如果 a 和 b 已经在同一个集合中,则忽略
sizee[find(b)]+= sizee[find(a)];
p[find(a)] = find(b);
}
else if(op[1] == '1'){
scanf("%d%d",&a,&b);
if(find(a) == find(b)) puts("Yes");
else puts("No");
}
else{
scanf("%d",&a);
printf("%d\n",sizee[find(a)]);
}
}
return 0;
}
还有一种时要用并查集维护到祖宗节点的距离,多用于解决类似食物链问题的动态连通性问题。
先给出此类并查集的板子:
nt p[N],d[N];//p表示父节点,d表示到父节点的距离 路径压缩后表示到根节点的距离
int n,m;
//得到根节点 并进行路径压缩更新d[x]为节点到根节点的距离
int find(int x){
if(p[x]!=x){
int t=find(p[x]);//得到根节点
d[x]+=d[p[x]];//更新距离为到根节点的距离
p[x]=t;//更新父节点为根节点
}
return p[x];
}
//初始化
for(int i=1;i<=n;i++){
p[i] = i;
d[i] = 0;
}
//合并a和b所在的两个集合:
p[find(a)] = find(b);
d[find(a)] = distance;//根据具体问题,初始化find(a)的偏移量
Acwing 240.食物链
输入样例:
100 7
1 101 1
2 1 2
2 2 3
2 3 3
1 1 3
2 3 1
1 5 5
输出样例:
3
实现思路:
- 由题意可知任意两个动物可能存在三种关系之一:被吃,吃,同类。给定两个动物x和y,若知道这两个动物与第三个动物z(中间人)的关系,则可推导出x和y的关系。如x吃z,z吃y,则可得y吃x,从而形成一个环形食物链;
- 考虑维护一个并查集。选择并查集的根节点(集合编号)为中间人·1,通过维护额外的信息来表示个节点与根节点的关系,就可以推断出集合中任意两个节点之间的关系。
-
- 这个额外的信息选择当前节点到根节点的距离,因为存在3种关系,则用距离mod 3就可以得到三种结果,以此来表示当前节点与根节点的关系;
-
- 设定mod 3 = 0表示节点与根节点是同类,mod 3 = 1表示吃根节点, mod 3 = 2 表示被根节点所吃。则mod 3 =2的节点就吃 mod 3= 1的节点,mod 3 值相同的节点是同类;
- -求每个节点到根节点的距离:在并查集的路径压缩过程中即设置每个结点的父节点为根节点时来完成。设置一个距离数组d[],d[x]表示当前结点与父节点的距离。首先查找当前结点的根节点记录下来,然后该结点到根节点的距离=该结点到父节点的距离d[x]+父节点到根节点的距离d[p[x]],再赋给d[x],那么d[x]就更新成为当前结点到根节点的距离。最后更新x的父节点p[x]为根节点。
- 对一句话判断假话还是真话
-
- 若给出的x,y大于编号最大值n,直接判断为假话;
-
- 否则,继续判断。先得到x和y的根节点。再根据两种说法分别判断:
-
-
- 对第一种说法:x和y是同类,进行判断。若在同一个集合,用两者到根节点的距离mod 3判断,取模的值不相等就不是同类,为假话;否则为真话,就要先合并到一个集合,然后更新x根节点到y根节点的距离满足x,y是同类。
-
-
-
-
- 判断:d[x]%3 != d[y] %3 -> (d[x] - d[y] )%3!=0 假话;
-
-
-
-
-
- 否则更新距离(假设x合并到y集合):由(d[x]+d[p[x]])%3 = d[y] %3 -> d[p[x]]=d[y] - d[x];
-
-
-
-
- 对第二种说法:x吃y。若在同一个集合,判断x到根节点的距离mod 3是否满足比y到根节点的距离mod 3大1,若不满足,则x吃y是假话;否则真话,就要先合并到同一个集合,然后更新x父节点到根节点的距离,满足x吃y是真话。
-
-
-
-
- 判断:d[x]%3 != d[y] %3 == 1 -> (d[x] - d[y] -1)%3!=0 假话;
-
-
-
-
-
- 否则更新距离:(假设x合并到y集合):由(d[x]+d[p[x]])%3 - d[y] %3 ==1 -> d[p[x]]=d[y] - d[x] + 1;
-
-
以上就是整个实现思路,下面给出具体实现代码(详解版):
#include <iostream>
using namespace std;
const int N = 50010;
int p[N],d[N];//p表示父节点,d表示到父节点的距离 路径压缩后表示到根节点的距离
int n,m;
//得到根节点,并进行路径压缩更新d[x]为节点到根节点的距离
int find(int x){
if(p[x] != x){
int u=find(p[x]);//得得到根节点
d[x] += d[p[x]];//更新距离为到根节点的距离
p[x] = u;//父节点更新为根节点
}
return p[x];
}
int main(){
cin >> n >> m;
for(int i = 1;i <= n;i ++) p[i] = i;
int res = 0;
while(m --){
int t,x,y;
cin >> t >> x >> y;
if(x > n || y > n) res ++;//假话
else{
int px = find(x),py = find(y);//得到各自根节点
if(t == 1){
if(px == py && (d[x] - d[y])%3) res ++;//假话
else if(px != py){//合并集合,更新距离
p[px] = py;
d[px] = d[y] - d[x];
}
}
else{
if(px == py && (d[x] - d[y] - 1)%3) res ++;//假话
else if(px != py){//合并集合,更新距离
p[px] = py;
d[px] = d[y] - d[x] +1;//满足x吃y
}
}
}
}
cout << res <<endl;
return 0;
}
这就是并查集的应用,总共三个板子,合并集合,询问是否在集合以及维护集合大小和到根节点的距离,可用于解决集合合并、连通块中点的数量以及食物链问题的动态连通性问题。