并查集算法实现
并查集算法即是 Union-Find 算法,主要解决图论中“动态连通性”问题。
“连通”是一种等价关系,即它具有如下 3 个性质:
- 自反性:节点 a 和 a 是连通的。
- 对称性:如果节点 a 和 b 连通,那么 b 和 a 也是连通的。
- 传递性:如果节点 a 和 b 连通,b 和 c 连通,那么 a 和 c 也是连通的。
一般使用森林来表示图的连通性,用数组来具体实现这个森林。在每个连通分支(树)中选择一个节点作为根节点,代表这个连通分支。
class UnionFind {
private:
int cnt; // 记录连通分支的数目
vector<int> parent; // 节点 i 的父节点是 parent[i]
// 返回节点 a 的根节点
int find(int a) {
// 根节点 a == parent[a]
while(parent[a] != a){
a = parent[a]; // 依次向上寻找 a 的父节点
}
return a;
}
public:
UnionFind(int n) {
cnt = n; // 初始时,所有节点均不连通
parent.resize(n);
for(int i=0; i<n; ++i){
parent[i] = i; // 节点 i 和 i 是连通的
}
}
// 连通节点 a 和 b
void unite(int a, int b) {
int root1 = find(a);
int root2 = find(b);
if(root1 == root2) // 节点 a 和 b 处于同一个连通分支
return;
parent[root1] = root2; // 将两个连通分支(树)合并成一个
--cnt;
}
// 判断节点 a 和 b 是否连通
bool connected(int a, int b) {
int root1 = find(a);
int root2 = find(b);
return root1 == root2; // 如果节点 a 和 b 的根节点相同,表示它们在同一个连通分支
}
// 返回连通分支的数量
int count() {
return cnt;
}
};
上面代码中,find、unite 和 connected 的最坏时间复杂度是 O ( n ) O(n) O(n),因为代表连通分支的树可能出现极度不平衡的情况,甚至退化成链表。
路径压缩
如果能压缩每棵树的高度,使树高保持为常数,如下图所示,这样 find 的时间复杂度就为 O ( 1 ) O(1) O(1)。
代码实现如下:
int find(int a) {
while(parent[a] != a){
parent[a] = parent[parent[a]]; // 进行路径压缩:节点 a 的父节点变为其原父节点的父节点
a = parent[a];
}
return a;
}
调用 find 函数每次向树的根节点遍历的同时,将树高缩短,最终所有的树高都不会超过 3。因此,find 的时间复杂度是 O ( 1 ) O(1) O(1),相应地,unite 和 connected 函数的时间复杂度都降为 O ( 1 ) O(1) O(1)。
并查集算法应用
例题:等式方程的可满足性
题目来源:https://leetcode-cn.com/problems/satisfiability-of-equality-equations
给定一个字符串数组 equations,其中的字符串表示变量之间的关系,每个字符串 equations[i] 的长度为 4,并采用两种不同的形式之一:“a==b” 或 “a!=b”。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。
只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false。
示例 1:
输入:["a==b","b!=a"]
输出:false
解释:如果我们指定,a = 1 且 b = 1,那么可以满足第一个方程,但无法满足第二个方程。没有办法分配变量同时满足这两个方程。
示例 2:
输入:["b==a","a==b"]
输出:true
解释:我们可以指定 a = 1 且 b = 1 以满足满足这两个方程。
提示:
1 <= equations.length <= 500
equations[i].length == 4
equations[i][0] 和 equations[i][3] 是小写字母
equations[i][1] 要么是 '=',要么是 '!'
equations[i][2] 是 '='
解题思路
题目中的 ==
关系具有自反性、对称性和传递性,所以它是一种等价关系,可以使用并查集算法解决问题。其中,所有相等的变量属于同一个连通分支。
首先遍历所有等式。因为同一等式中的两个变量属于一个连通分支,因此将两个变量进行合并,调用 unite 函数。
再遍历所有不等式。不等式中的两个变量应属于不同的连通分支,查找两个变量所在连通分支,判断它们是否在同一连通分支中,调用 connected 函数,如果在同一分支中,则产生矛盾。最终结果返回 false。
bool equationsPossible(vector<string>& equations) {
UnionFind uf = UnionFind(26); // 26 个小写字母
for(int i=0; i<equations.size(); ++i){
if(equations[i][1]=='='){
int a = equations[i][0]-'a';
int b = equations[i][3]-'a';
uf.unite(a, b);
}
}
for(int i=0; i<equations.size(); ++i){
if(equations[i][1]=='!'){
int a = equations[i][0]-'a';
int b = equations[i][3]-'a';
if(uf.connected(a, b)){ // 节点 a 和 b 在同一连通分支中,矛盾
return false;
}
}
}
return true;
}
使用路径压缩中的 find 函数,则 unite 和 connected 函数的时间复杂度为
O
(
1
)
O(1)
O(1)。因此,上面程序的时间复杂度是
O
(
n
)
O(n)
O(n),
n
n
n 表示 equations 的长度。
空间复杂度是
O
(
k
)
O(k)
O(k),
k
k
k 表示 UnionFind 中 parent 的长度。