算法概述
《Algorithms》(4th)在第一章第五节介绍了并查集算法(使用路径压缩的加权 quick - union算法)。该算法主要用于高效求解动态连通性相关问题:
- 判断指定两点是否连通。
- 计算连通分量个数。
适用条件
- 点集固定。
- 只允许动态添加连通关系,不可动态删除连通关系。
API
public class |
UF |
功能 |
---|---|---|
UF(int N) |
初始化 N 个点,序号为 0 ~ N - 1 | |
void |
union(int p, int q) |
在点 p 与点 q 之间添加连通关系 |
int |
find(int p) |
查询点 p 所在连通分量的标识符 |
boolean |
connected(int p, int q) |
判断点 p 与点 q 是否连通 |
int |
count() |
查询连通分量的数量 |
实现代码(Java)
public class UF {
private int[] id;
private int[] sz;
private int count;
public UF(int N) {
count = N;
id = new int[N];
for (int i = 0; i < N; ++i)
id[i] = i;
sz = new int[N];
for (int i = 0; i < N; ++i)
sz[i] = 1;
}
public void union(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot)
return;
if (sz[pRoot] < sz[qRoot]) {
id[pRoot] = qRoot;
sz[qRoot] = sz[pRoot] + sz[qRoot];
} else {
id[qRoot] = pRoot;
sz[pRoot] = sz[pRoot] + sz[qRoot];
}
count = count - 1;
}
public int find(int p) {
int oldP = p, tmp;
while (p != id[p])
p = id[p];
while (oldP != id[oldP]) {
tmp = id[oldP];
id[oldP] = p;
oldP = tmp;
}
return p;
}
public boolean connected(int p, int q) {
return find(p) == find(q);
}
public int count() {
return count;
}
}
算法详解
算法使用树(有根树)表达连通分量,使用森林表示整个集合。
每个点都有对应的 id
与 sz
值。id
为该点在树中的父亲的序号,树根的 id
指向自身。只有树根的 sz
值有意义,代表树的大小,即连通分量中点的数目。count
变量用于记录森林中树的个数。
int count()
函数直接返回 count
变量,即连通分量的个数。
boolean connected(int p, int p)
函数中,先寻找两个点所在树的树根。
- 如果树根序号一致,表示两者在一棵树(连通分量)中,返回
true
。 - 如果树根序号不同,返回
false
。
void union(int p, int q)
函数中,首先获取两个点对应的树根。
- 如果两点树根相同,则已在同一连通分量中,为了保证树结构的性质,直接退出,不再次连接两点。
- 如果不在同一分量中,需要将一棵树设置为另一棵树的子树,合并两个连通分量。为了保证树的平衡性,需要将规模较小的树并入规模较大的树中,更新小树树根的
id
,更新合并之后的树根的sz
,更新count
连通分量个数。
int find(int p)
函数中首先备份查询点的序号,然后查询至根节点。接下来将路径上所有的节点的 id
都设置为根节点,压缩树的高度。
性能分析
该算法的所有操作的均摊时间复杂度在反 Ackermann 函数(α(x))范围之内。对于任何实际的有意义的数字,α(x) 小于 5。