并查集 (Union-Find) 算法


并查集算法实现

并查集算法即是 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)

2
0
3
7
5

代码实现如下:

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 的长度。


刷题题解目录

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值