文章目录
1. 动态连接问题
实际问题
Union Find 常用来解决动态连接 (Dynamic connectivity) 问题。在很多实际应用中,都存在动态连接问题,比如:
- 一张图里面的像素点的相关操作
- 网络中的计算机
- 社交网络中的好友关系
- 数学集合中的元素
- …
抽象问题
简单来说,动态连接问题可以抽象为如下问题:
有若 N 个节点 0,1,…,n-1,希望对它能有如下操作:
- Union:将两个节点连接在一起
- Find / connected query:询问两个节点是否连通
比如经过如下操作后:
union(4, 3)
union(3, 8)
union(6, 5)
union(9, 4)
union(2, 1)
可以得到连通图:
此时,
connected(0, 7) //false
connected(8, 9) //true
如果再做如下连通操作:
union(5, 0)
union(7, 2)
union(6, 1)
union(1, 0)
此时更新后的连通图如下:

此时,
connected(0, 7) //true
直观的来说,动态连接问题应该有如下等价关系:
- 自等性:
connected(p, p) //true
- 对称性
if(connected(p, q))
then connected(q, p) == true
- 传递性
if(connected(p, q) && connected(q, r))
then connected(p, r) == true
2. 解决思路
连通集
我们可以维护若干个连通集,每个连通集内部的元素都相互连通,不属于同一个连通集的元素则尚未连通:
所以,现在的问题就变成,如何确定并实时更新连通集??
类的设计
- 用节点数 N 来构造一个
UF类实例 union函数用来将两个节点连通在一起connected函数用来查询两个节点是否连通- 注意这里并没有涉及到节点内部的具体细节,每一个节点是用一个 index 来表示,那节点和 index 之间的关系需要另外保存(比如可以用一个 map, 或者 hashTable)
3. 代码实现
3.1 Quick Find
数据结构
- 用一个大小为
N的整数数组id[N] - 当且仅当
id[p] == id[q]时,说明 p, q 连通
逻辑实现
- 初始时,
id[i] = i,表明每一个节点属于一个单独的连通集,各个节点之间互不连通:
- 做
union(p, q)操作时,将id[p]和id[q]的值设为一致,注意要将当前id[p]所在的连通集合内的所有元素都加入id[q]所在的连通集。(当然你也可以反过来添加)
connected(p, q)函数只需要判断id[p] ==? id[q]
代码实现
class QuickFindUF {
public:
QuickFindUF(int n) {
eleCount = n;
id = new int[eleCount];
//初始化为不同集合
for (int i = 0; i < eleCount; ++i) {
id[i] = i;
}
}
void union(int p, int q) {
if (!connected(p, q)) {
int pid = id[p];
int qid = id[q];
//遍历查找 p 所在的集合,然后将其加入到 q 所在的集合
for (int i = 0; i < eleCount; ++i) {
if (id[i] == pid) {
id[i] = qid;
}
}
}
}
bool connected(int p, int q) {
return id[p] == id[q];
}
~QuickFindUF() {
delete[] id;
}
private:
int *id;
int eleCount;
};
性能分析
| algorithm | initialize | union | find |
|---|---|---|---|
| quick find | n | n | 1 |
- 可以看到,union 操作时间效率是 O(n),那如果我们进行 n 次 union 操作,时间效率将会是平方指数级别,效率太低
3.2 Quick Union
数据结构
- 用一个大小为
N的整数数组id[N] id[p]中存储的是它的父节点的 index,也就是整个的会呈现出树状的关系- 如果
id[p] = p, 说明p节点就是根节点
逻辑实现
- 初始时,
id[i] = i,表明每一个节点属于一个单独的连通集,各个节点之间互不连通:
- 做
union(p, q)操作时,找到p的根节点pRoot以及q的根节点qRoot,将pRoot挂在qRoot上,使之成为qRoot的子节点。(当然你也可以反过来挂)
代码实现
class QuickUnionUF {
public:
QuickUnionUF(int n) : eleCount(n) {
id = new int[eleCount];
//初始化为 n 个不同的连通集
for (int i = 0; i < eleCount; ++i) {
id[i] = i;
}
}
void unionTwo(int p, int q) {
//找到 p 的根节点合 q 的根节点
int pRoot = getRoot(p), qRoot = getRoot(q);
//把 pRoot 挂到 qRoot 上
id[pRoot] = qRoot;
}
bool connected(int p, int q) {
return getRoot(p) == getRoot(q);
}
~QuickUnionUF() {
delete[] id;
}
private:
//计算 p 的根节点
int getRoot(int p) {
while (p != id[p]) {
p = id[p];
}
return p;
}
private:
int *id;
int eleCount;
};
性能分析
| algorithm | initialize | union | find |
|---|---|---|---|
| quick find | n | n | 1 |
| quick union | n | n (worst) | n (worst) |
- 就平均性能而言,quick union 的
union操作应该是比 quick find 的union操作要快的,但是find操作却比 quick find 的find操作要慢 - 在最差的情况下, 如果连通集被连成了一个链表结构,此时需要遍历整个链表来寻找根节点,所以此时对于
union和find操作都会变成 O(n),性能变差
3.3 Weighted Quick Union
quick union 最大的问题,就是所构建的 tree 的高度可能会很高(极端情况下变成链表),导致查找 root 时效率低,所以一个很直观的解决办法,就是 union 操作构建树结构的时候,尽量让 smaller tree 挂在 big tree 上,这样构建的树的高度就会降低:
数据结构
- 在 quick union 的基础上,多维护一个
size[eleCount]数组,size[i]用来记录以节点i为根节点的树的大小,注意这里的大小用树中元素的个数表示,而不是树的高度。
代码实现
connected操作不变union操作只需要多记录一下树的大小
void unionTwo(int p, int q) {
//找到 p 的根节点合 q 的根节点
int pRoot = getRoot(p), qRoot = getRoot(q);
if(pRoot == qRoot)
return;
//p 所在的树小,q 所在的树大
//将 p 树挂到 q 上
if(size[pRoot] < size[qRoot]){
id[pRoot] = qRoot;
size[qRoot] += size[pRoot];
}
else{
id[qRoot] = pRoot;
size[pRoot] += size[qRoot];
}
}
性能分析
| algorithm | initialize | union | find |
|---|---|---|---|
| quick find | n | n | 1 |
| quick union | n | n (worst) | n (worst) |
| weight quick union | n | log2n | log2n |
3.4 Path Compression
上面的 weight quick union 看上去已经很不错了,但是,优化之路并没有结束,这个问题还可以继续优化。
在查找节点 p 的根节点 root 时,我们会从节点 p 出发,一直向上查找祖辈节点,直到根节点。那我们完全可以在遍历的时候,顺手就把路径上某个节点 i 的父节点指向更远的祖辈节点,压缩路径,进一步减少了树的高度。
代码实现
//计算 p 的根节点
int getRoot(int p) {
while (p != id[p]) {
//设置 p 的父节点是其祖父节点
id[p] = id[id[p]];
p = id[p];
}
return p;
}
只需要一行代码就可以实现路径的压缩,何乐而不为呢!!
4. 实际应用
有这样一个物理连通性问题抽象如下:
- 有 N X N 的矩阵,每个格子有打开和未打开两种状态
- 打开的概率为 p,未打开的概率就是 1 - p
现在想知道,整个 N 阶矩阵连通的概率是多少?连通是指从上到下至少有一条连通的路径:

Union Find 解决
- 初始化:每一个格子作为一个节点,将互相连通的格子用 union 操作将对应的节点连接起来

- 每添加一个打开的节点,就需要与它相邻的打开的节点进行连通

- 判断上下节点之间是否有连通的。如果采用暴力解法,那么时间复杂度将会达到 O(n2)。这里可以在上下分别添加一个节点,分别和最上行和最下行的节点相连,那么我们只需要判断这两个节点是否连通就可以了

本文介绍了并查集这一数据结构,用于解决动态连接问题,如像素操作、网络计算机、社交网络好友关系等。文中详细讲解了Quick Find、Quick Union、Weighted Quick Union和Path Compression四种实现方式,并分析了它们的性能和实际应用。
4330

被折叠的 条评论
为什么被折叠?



