并查集
主要是用以解决元素分组的问题
其基本操作包括
- 合并:把两个不相交的集合合并为一个集合
- 查询:查询两个元素时否在一个集合中
在实际算法问题中有一个使用并查集的经典问题
1、在最开始我们有一系列的点[0, 1, 2, 3, 4, 5, 6, …]作为问题的元素,一个标号可以代表一个人,在最开始我们不知道他们的亲源关系时,大家都是孤立的节点,即单元素集合
2、 随后我们得到一系列的关系列表例如[[0, 1], [0, 3], [3, 5], …],该列表中的每个元素都是一个关系, 这些关系就如同两个元素(两个点)之间的有向线,通过不断的建立这些关系线,我们将会得到一个基本的关系图
3.、但是,这样的关系图只是标名了父子关系而不是祖先关系。因此就需要递归的继续寻找上一节点的祖先即找爸爸的爸爸,直到找到初代祖先。
4、 我们用一个数组来表示他们的祖先关系,fa[i]=j
,表示i
点的祖先是下标为j
点的元素
- 初始状态
fa[i] = i;
// 初始时,每个人都是孤元素集合,此时关系指向自己
查询
int find (x) {
if (fa[x] == x) { // 若祖先==自己,自己就是根祖先
return x;
} else { // 否则,继续往上找上一个祖先的祖先
fa[x] = find(fa[x]); // 路径压缩
return fa[x];
}
}
合并
基于上面的查询算法已经可以知道每个人的祖先了,但如果这个时候有两个家族打算合并,那么只要把两个家族的祖先变成父子关系,就如同两个数型结构,要合并的话,只有把一个树的根节点作为另一颗树的子节点就可以形成一个新的树型结构。
void unionSet(int x, int y) {
// x 与 y 所在家族合并
x = find(x);
y = find(y);
fa[x] = y; // 把 x 的祖先变成 y 的祖先的儿子
- 但两个祖先,谁当父谁当子会更好呢,还是都一样?
当然是不一样的,两个家族的合并,从常理来说,肯定是家族人少的一家合到人多的一家会方便点,就树型结构来说,把树深度较低(或个数较少)的树作为另一颗深度大的子树可可以保持层数不变,同时在执行查找操作的用时更小,反过来的话树就会越来越深
因此我们需要额外的变量保存这些树的高度
std::vector<int> size(N, 1); // 记录并初始化子树的大小为 1
void unionSet(int x, int y) {
int xx = find(x), yy = find(y);
if (xx == yy) return;
if (size[xx] > size[yy]) // 保证小的合到大的里
swap(xx, yy);
fa[xx] = yy;
size[yy] += size[xx];
}
补充
在查找的方法中采用了路径压缩
的优化
在合并的方法中采用了启发式
合并的优化