并查集 一种快速处理集合查找和合并的数据结构
并查集的用途
顾名思义,并查集主要的作用就是用来处理集合的合并与集合元素的查找(判定元素是否在该集合中),其合并和查找操作时间复杂度可以近似看作O(1)。并查集其实是一种“使用树存储集合”的思想,树的root节点是整个集合的标志,区别不同集合的方式就是判断树的root节点是否相同。
左边的树的root节点为1,则该集合可认为是集合1,右侧为集合4。在使用树作为存储形式时,如果想知道3所属的集合是哪个,只需要寻找一层层寻找3的祖宗节点,直至root节点,则可以判断该节点所属集合。(对于root节点来说,它的父节点是它本身,所以只需要递归寻找到父节点和本身相同的节点即可停止递归)
在合并集合时,只需要将root节点变更为另一集合的子节点。此时集合1中的所有元素的root节点为4,则他们都属于集合4。
路经压缩
在上述算法中,查找元素的时间复杂度需要取决于树的高度O(h),我们在确定某个元素属于集合时,可以将路径上所有的祖宗节点的父节点更换为root节点。比如我们查找到节点5属于集合1,那么就将查找路径上所有节点的指向更改。
该优化称为路经压缩,经过优化后,再次查找节点5所属集合,只需一次查找,时间复杂度为O(1)。
代码实现
例题参考:https://www.acwing.com/problem/content/838/
#include<iostream>
using namespace std;
const int N = 100000 + 10;
int p[N]; // p[i] 为 i 的父节点
int find(int x) {
// 寻找x所属集合,同时进行路径压缩
if (x == p[x]) return x;
p[x] = find(p[x]);
return p[x];
}
int main() {
// 初始时,每个数各自作为一个集合,父节点是自己
for (int i = 0; i < N; i ++) p[i] = i;
int n, m;
scanf("%d%d", &n, &m);
while (m -- ) {
char op[2];
int a, b;
scanf("%s%d%d", op, &a, &b);
if (op[0] == 'M') {
// 合并集合时,只需要将集合1的root节点的父节点指向为另一个集合的root节点
p[find(a)] = find(b);
} else {
if (find(a) == find(b)) printf("Yes\n");
else printf("No\n");
}
}
}
并查集的增强
在原有并查集的基础上,我们可以通过增加空间来记录更多的信息。例如我们可以维护每个集合的元素个数,这只需要在集合合并时,个数相加。我们还可以维护每个节点到root节点的距离,这种方式也叫做带权重的并查集。