动态连通性问题 “连通” 是一种等价关系,两个对象 p 和 q 是相连通的,意味着它具有:
自反性:p 和 p 是相连的 对称性:如果 p 和 q 是相连的,那么 q 和 p 也是相连的 传递性:如果 p 和 q 相连且 q 和 r 相连,那么 p 和 r 也是相连的 等价关系将对象分为多个等价类,在这个,当且仅当两个对象相连通时才属于同一个等价类。
使用一个数据结构保存 n 个对象的连通关系,并用来判断一对新的对象是否相连,这样的问题就是动态连通性问题,该数据结构叫做 并查集 。
在网络中等价类叫做连通分量。
并查集的操作 init(): 初始化所有对象 find(p): 返回 p 所在的连通分量 union(p, q): 在 p 和 q 直接添加一条连接 connected(p, q): 如果 p 和 q 在同一连通分量则返回 true count(): 连通分量的数量 普通并查集 并查集使用数组保存每个对象的关系,对象 p 作为数组的索引,值为连通分量的 id 。
init 时每个对象都不连通,即连通分量 id 都为自身,cnt 记录连通分量数量
Copy 1
2
3
4
5
6
void init (vector< int >& uf) {
for (int i = 0 ; i < uf.size(); i++ ) {
uf[i] = i;
}
cnt = uf.size()
}
find 时直接返回对应的连通分量
Copy 1
2
3
int find (vector< int >& uf, int p) {
return uf[p];
}
union 时如果 p 和 q 的连通分量不同,将 q 的连通分量下所有对象的连通分量 id 改为 p 的连通分量 id
Copy 1
2
3
4
5
6
7
8
9
10
11
12
13
void un (vector< int >& uf, int p, int q) {
int pId = find(p);
int qId = find(q);
if (pId == qId) {
return ;
}
for (int i = 0 ; i < uf.size(); i++ ) {
if (uf[i] == qId) {
uf[i] = pId;
}
}
cnt-- ;
}
connected 只需判断 p 和 q 的连通分量 id 是否相同
Copy 1
2
3
bool connected (vector< int >& uf, int p, int q) {
return find(p) == find(q);
}
count 时返回连通分量的计数
Copy 1
2
3
int count () {
return cnt;
}
find-union 优化 对象 p 在数组中记录的值 uf[p] 指向同一连通分量的其它对象,根对象指向它自己。
因此 find 时需要一直查找到根对象
Copy 1
2
3
4
5
6
int find (vector< int >& uf, int p) {
while (uf[p] != p) {
p = uf[p];
}
return uf[p];
}
union 时只需要将 p 和 q 的根对象统一
Copy 1
2
3
4
5
6
7
8
9
10
void un (vector< int >& uf, int p, int q)
{
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot) {
return ;
}
uf[qRoot] = pRoot;
cnt-- ;
}
这样处理后,这个 uf 的结构就是一个森林,每个连通分量就是森林里的一颗树
加权 union 算法 在 union 时将小的连通分量连接到大的连通分量上,减少树的深度,使用额外的数组记录连通分量节点数量,初始化时为 1
Copy 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void un (vector< int >& uf, vector< int >& w, int p, int q)
{
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot) {
return ;
}
if (w[pRoot] > w[qRoot]) {
uf[qRoot] = pRoot;
w[pRoot] += w[qRoot];
} else {
uf[pRoot] = qRoot;
w[qRoot] += w[pRoot];
}
cnt-- ;
}
路径压缩的 find 算法 即将所有节点连接到根节点上,在 find 时实现
Copy 1
2
3
4
5
6
7
8
9
10
11
12
13
int find (vector< int >& uf, int p)
{
int pRoot = p;
while (uf[pRoot] != pRoot) {
pRoot = uf[pRoot];
}
// 路径压缩
while (uf[p] != p) {
p = uf[p];
uf[p] = pRoot;
}
return p;
}
递归版本:
Copy 1
2
3
4
5
6
7
8
int find (vector< int >& uf, int p)
{
if (uf[p] != p) {
uf[p] = find(uf, uf[p]);
}
return uf[p];
}
实战 leetcode 1971: https://leetcode.cn/problems/find-if-path-exists-in-graph/description/
Copy 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
public :
int ufFind(vector< int >& uf, int p) {
int start = p;
while (uf[p] != p) {
p = uf[p];
}
while (uf[start] != p) {
start = uf[start];
uf[start] = p;
}
return p;
}
void ufUnion (vector< int >& uf, int p, int q) {
int pRoot = ufFind(uf, p);
int qRoot = ufFind(uf, q);
if (pRoot == qRoot) {
return ;
}
uf[pRoot] = qRoot;
}
bool validPath (int n, vector< vector< int >>& edges, int source, int destination) {
vector< int > uf(n);
for (int i = 0 ; i < n; i++ ) {
uf[i] = i;
}
for (const auto & edge : edges) {
ufUnion(uf, edge[0 ], edge[1 ]);
}
return ufFind(uf, source) == ufFind(uf, destination);
}
};