并查集(Disjoint-set data structure)是一种树型
的数据结构,用于处理一些不交集(Disjoint sets,一系列没有重复元素的集合)的合并及查询问题。
并查集的实现:
开始时候森林中每个元素之间是相互独立的,也就是说初始时每个元素都是一个集合
。如果某些元素在同一个集合中,那么它们就在同一颗树上。我们通常使用数组来存储这个森林
。
- 数组的
下标表示元素的编号
- 数组中的某个值如果是
正数
的话,说明该元素的父节点
的编号就是这个正数 - 是
负数
的话,就说明该元素是一个根节点
,并且负数的绝对值表示它所在的集合的节点个数
- 因此在初始化的时候数组中的各个值为
-1
,表示自己是一个独立的集合
并查集主要有两个操作:
merge
:merge (合并)操作会先寻找 a 和 b 的根节点,如果不是同一个根节点,就将 a 所在的集合与 b 所在的集合合并。这里所说的合并具体操作为令 a 的根节点指向 b 的根节点或令 b 的根节点指向 a 的根节点find
:find 操作会向上追溯来查询该节点的根节点是哪个
怎么看两个节点是否在同一个集合?
查询两个节点的根节点是不是一样的就行了。
merge 操作时为什么要先寻找 a 和 b 的根节点再进行合并?
假设把 a 所在的集合合并到 b所在的集合中。如果 a 是叶子节点,直接合并的话,会丢掉 a 的父节点以及父节点上面的结点。
merge 操作中让 a , b 合并,到底是令 a 的祖先指向 b 的祖先,还是令 b 的祖先指向 a 的祖先?
因为树越高的话,查询的效率就越低。因此通常使小树指向大树(或者低树指向高树)
优化的思路:
为了保证查询效率,避免合并树的时候退化成链表,我们需要将树尽量“矮胖”。
按秩合并
为了让树尽可能“矮胖”,我们在合并时需要把高度小的树拼接到高度大的树上。这里可以把树的高度称为秩(rank)。如果两棵树的秩不同,那么合并之后的树的秩等于原来较高的那个树的秩;如果两棵树的秩相同,那么合并后的新树的秩等于原来的秩再加 1 。
路径压缩
当我们在查找某个节点的根节点时,借助父节点指针向上追溯查找根节点的过程中,我们可以将所经过路径上的所有节点的父节点指针都更新为指向根节点。这样,下次查询根节点的时候就更快了。
/*
Author: ZL
Description: Disjoint-set data structure
Date: 2021-8-15
*/
class DisjointSet {
private:
std::vector<int> tree;
public:
DisjointSet(int size) {
tree = std::vector<int>(size, -1); //数组中的值初始化为-1
}
bool merge(int a, int b) {
int root_a = find(a);
int root_b = find(b);
if (root_a == root_b) return false;
if (tree[root_a] < tree[root_b]) {
tree[root_a] += tree[root_b];
tree[root_b] = root_a;
} else {
tree[root_b] += tree[root_a];
tree[root_a] = root_b;
}
return true;
}
int find(int x) {
int root = x, t;
while (tree[root] >= 0) {
root = tree[root];
}
//路径压缩
while (x != root) {
t = tree[x]; //保存一下父节点
tree[x] = root;
x = t;
}
return root;
}
};