0%

并查集入门

概述

并查集是一种非常高效的查找算法,它多用于元素分组的问题。它主要包含两种操作,第一种是合并集合(并),第二种是查找两个元素是否属于同一集合(查)。

并查集的重要思想在于用集合中的一个元素代表集合。并查集的底层结构类似于堆,利用数组来存储一个树的结构。但不同的是,堆是一棵独立的二叉树,并查集的树是多叉树,而且可能不止一棵树(也就是森林)。在并查集中,我们把相互独立的数据集称为连通分量,连通分量在逻辑上可以形象地描述为一棵树,每棵树都有一个根节点,其他的元素都会直接或间接指向根节点。

举个例子,例如这个数组:[0,2,2,2,0,1],对应下标为0-5。这个数组称作parent数组,即存储父节点。初始化时a[i]=i,即每个节点的父亲都是自己(或者说每个节点都是孤立的)。随后经过一系列的合并操作,我们的数组最终从孤立的点结构形成了树结构。

图片来自https://segmentfault.com/a/1190000022952886

(图片来自https://segmentfault.com/a/1190000022952886)

以第一棵树为例,5的父节点是1,故a[5]=1,然后1的父节点是2,则a[1]=2(a[a[5]]=2),2是根节点,故a[2]=2(a[a[a5]]]=2)最后,数组每个元素都代表其指向的父节点。

实现(c语言实现)

并查集的实现代码非常的简短和精妙。这里先给出最基本的一种实现:

1
2
3
4
5
6
7
8
9
10
11
int fa[MAXN];
void init(int n) {
for(int i = 0; i < n; i++) fa[i] = i;
}
int find(int x) {
if(fa[x] == x) return x;
else return find(fa[x]);
}
void union(int x, int y) {
fa[find(x)] = fa[find(y)];
}

这种写法可以说是非常精简了。但是这种写法导致的问题是,find操作的效率较低,因为随着合并操作的进行,整个并查集可能会形成一个链的结构,从叶节点查询到父节点的时间复杂度就会升高。

这里有两种优化方法。一个是优化find,一个是优化union。优化find可以考虑路径压缩,即将路径上的每个节点的父节点都指向根节点。优化union可以考虑按秩合并(秩在这里可以理解为树的深度,初始化所有元素的秩为1)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int fa[MAXN];
int rank[MAXN];
void init(int n) {
for(int i = 0; i < n; i++) {
fa[i] = i;
rank[i] = 1;
}
}

int find(int x) {
if(fa[x] == x) return x;
else return fa[x] = find(fa[x]); // 父节点设为根节点同时返回父节点
}

void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);

if(rootX == rootY) return;

if(rank[rootX] <= rank[rootY]) {
fa[rootX] = rootY;
} else {
fa[rootY] = rootX;
}

if(rank[rootX] == rank[rootY] && x != y) rank[rootY]++;
}

举例

亲戚问题

(洛谷P1551)亲戚

题目背景
若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
题目描述
规定: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个询问的答案为“具有”或“不具有”亲戚关系。

这个题很明显的就是并查集问题。每一行读取亲戚关系的时候,就将两个亲戚合并入并查集;每一行查找两个人是否是亲戚关系(有相同的根节点)的时候,就分别执行查找操作查找其根节点并比较结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <stdio.h>
#define MAXN 5001
int fa[MAXN];
int rank[MAXN];

void init(int n) {
for(int i = 1; i <= n; i++) {
fa[i] = i;
rank[i] = 1;
}
}

int find(int x) {
if(fa[x] == x) return x;
else return fa[x] = find(fa[x]); // 父节点设为根节点同时返回父节点
}

void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);

if(rootX == rootY) return;

if(rank[rootX] <= rank[rootY]) {
fa[rootX] = rootY;
} else {
fa[rootY] = rootX;
}

if(rank[rootX] == rank[rootY] && x != y) rank[rootY]++;
}

int main(){
int m, n, p;
scanf("%d %d %d", &n, &m, &p);
init(n);
for(int i = 0; i < m; i++) {
int Mi, Mj;
scanf("%d %d", &Mi, &Mj);
union(Mi, Mj);
}
for(int i = 0; i < p; i++) {
int Pi, Pj;
scanf("%d %d", &Pi, &pj);
if(find(Pi) == find(Pj)) printf("Yes\n");
else printf("No\n");
}

return 0;

}