11# 并查集
22
3- 关于并查集的题目不少,官方给的数据是 30 道(截止 2020-02-20),但是有一些题目虽然官方没有贴 ` 并查集 ` 标签,但是使用并查集来说确非常简单。这类题目如果掌握模板,那么刷这种题会非常快,并且犯错的概率会大大降低,这就是模板的好处。
3+ ## 背景
44
5- 我这里总结了几道并查集的题目:
5+ 相信大家都玩过下面的迷宫游戏。你的目标是从地图的某一个角落移动到地图的出口。规则很简单,仅仅你不能穿过墙。
66
7- - [ 547. 朋友圈] ( ../problems/547.friend-circles.md )
8- - [ 721. 账户合并] ( https://leetcode-cn.com/problems/accounts-merge/solution/mo-ban-ti-bing-cha-ji-python3-by-fe-lucifer-3/ )
9- - [ 990. 等式方程的可满足性] ( https://github.com/azl397985856/leetcode/issues/304 )
10- - [ 1202. 交换字符串中的元素] ( https://leetcode-cn.com/problems/smallest-string-with-swaps/ )
11- - [ 1697. 检查边长度限制的路径是否存在] ( https://leetcode-cn.com/problems/checking-existence-of-edge-length-limited-paths/ )
7+ ![ ] ( https://tva1.sinaimg.cn/large/008eGmZEly1goxczig610j30as0ar48s.jpg )
128
13- 上面的题目前面四道都是无权图的连通性问题,第五道题是带权图的连通性问题。两种类型大家都要会,上面的题目关键字都是** 连通性** ,代码都是套模板。看完这里的内容,建议拿上面的题目练下手,检测一下学习成果。
9+ 实际上,这道题并不能够使用并查集来解决。 不过如果我将规则变成,“是否存在一条从入口到出口的路径”,那么这就是一个简单的联通问题,只不过用肉眼去观察实在是太费力了。幸好,我们有计算器可以帮助我们。并查集算法就可以用来解决这个问题,并且比暴力枚举效率高。如果地图不变,而不断改变入口和出口的位置,并查集的效果高的超出你的想象。
10+
11+ 另外并查集还可以在人工智能中用作图像人脸识别等,感兴趣的可以查找一下相关资料。
1412
1513## 概述
1614
17- 并查集是一种树型的数据结构 ,用于处理一些不交集(Disjoint Sets)的合并及查询问题。有一个联合-查找算法(Union-find Algorithm)定义了两个用于此数据结构的操作:
15+ 并查集使用的是一种 ** 树型 ** 的数据结构 ,用于处理一些不交集(Disjoint Sets)的合并及查询问题。比如让你求两个人是否间接认识,两个地点之间是否有至少一条路径。上面的例子其实都可以抽象为联通性问题。即如果两个点联通,那么这两个点就有至少一条路径能够将其连接起来。值得注意的是,并查集只能回答“联通与否”,而不能回答诸如“具体的联通路径是什么”。如果要回答“具体的联通路径是什么”这个问题,则需要借助其他算法,比如广度优先遍历。
1816
19- - Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
20- - Union:将两个子集合并成同一个集合。
17+ 并查集(Union-find Algorithm)定义了两个用于此数据结构的操作:
2118
22- 初始化每一个点都是一个连通域,类似下图:
23-
24- ![ ] ( https://tva1.sinaimg.cn/large/008eGmZEly1gmm4f8vpp3j30p9024jra.jpg )
19+ - Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
2520
26- 由于支持这两种操作,一个不相交集也常被称为联合-查找数据结构(Union-find Data Structure)或合并-查找集合(Merge-find Set)。为了更加精确的定义这些方法,需要定义如何表示集合。一种常用的策略是为每个集合选定一个固定的元素,称为代表,以表示整个集合。接着,Find(x) 返回 x 所属集合的代表,而 Union 使用两个集合的代表作为参数 。
21+ - Union:将两个子集合并成同一个集合 。
2722
2823## 形象解释
2924
3025比如有两个司令。 司令下有若干军长,军长下有若干师长。。。
3126
32- 我们如何判断某两个师长是否属于同一个司令呢(连通性)?
27+ ### 判断两个节点是否联通
28+
29+ 我们如何判断某两个师长是否归同一个司令管呢(连通性)?
3330
3431![ ] ( https://tva1.sinaimg.cn/large/007S8ZIlly1ghlufxh5lhj30gs0bzwet.jpg )
3532
36- 很简单,我们顺着师长,往上找,找到司令。 如果两个师长找到的是同一个司令,那么就属于同一个司令。我们用 parent [ x ] = y 表示 x 的 parent 是 y,通过不断沿着搜索 parent 搜索找到 root,然后比较 root 是否相同即可得出结论 。
33+ 很简单,我们顺着师长,往上找,找到司令。 如果两个师长找到的是同一个司令,那么两个人就归同一个司令管 。
3734
38- 以上过程涉及了两个基本操作 ` find ` 和 ` connnected ` 。 并查集除了这两个基本操作,还有一个是 ` union ` 。即将两个集合合并为同一个 。
35+ 代码上我们可以用 parent [ x ] = y 表示 x 的 parent 是 y,通过不断沿着搜索 parent 搜索找到 root,然后比较 root 是否相同即可得出结论。 这里的 root 实际上就是上文提到的 ** 集合代表 ** 。
3936
40- > 为了使得合并之后的树尽可能平衡,一般选择将小树挂载到大树上面,之后的代码模板会体现这一点
37+ 这个不断往上找的操作,我们一般称为 find,使用 ta 我们可以很容易地求出两个节点是否连通。
38+
39+ ### 合并两个联通区域
4140
4241如图有两个司令:
4342
5150
5251## 核心 API
5352
53+ 首先我们初始化每一个点都是一个连通域,类似下图:
54+
55+ ![ ] ( https://tva1.sinaimg.cn/large/008eGmZEly1gmm4f8vpp3j30p9024jra.jpg )
56+
57+ 为了更加精确的定义这些方法,需要定义如何表示集合。一种常用的策略是为每个集合选定一个固定的元素,称为代表,以表示整个集合。接着,Find(x) 返回 x 所属集合的代表,而 Union 使用两个集合的代表作为参数进行合并。
58+
59+ > 这里的代表就是上面的“司令”。
60+
5461### find
5562
5663``` python
@@ -70,12 +77,16 @@ def find(self, x):
7077 return x
7178```
7279
73- (这里我进行了路径的压缩)
80+ 这里我在递归实现的 find 过程进行了路径的压缩,每次往上查找之后都会将树的高度降低到 2。
81+
82+ 这有什么用呢?我们知道每次 find 都会从当前节点往上不断搜索,直到到达根节点,因此 find 的时间复杂度大致相等于节点的深度,最坏的情况节点的深度等于树的高度,树的高度如果不加控制则可能为节点数,因此 find 的时间复杂度可能会退化到 $O(n)$。而如果进行路径压缩,那么树的平均高度不会超过 $logn$,具体证明略。不过给大家画了一个图来辅助大家理解。
7483
7584比如对于如下的一个图:
7685
7786![ ] ( https://tva1.sinaimg.cn/large/008eGmZEly1gmm4g5jyb9j30og05naaa.jpg )
7887
88+ 图中 r:1 表示 秩为 1,r 是 rank 的简写。这里的秩其实对应的就是上文的 size。(下同,不再赘述)
89+
7990调用 find(0) 会逐步找到 3 ,在找到 3 的过程上会将路径上的节点都指向根节点。
8091
8192![ ] ( https://tva1.sinaimg.cn/large/008eGmZEly1gmm4i1vrclg30ni05wtj9.gif )
@@ -101,10 +112,12 @@ def connected(self, p, q):
101112
102113![ ] ( https://tva1.sinaimg.cn/large/008eGmZEly1gmm4avz4iej30lv04rmx9.jpg )
103114
104- 图中 r:1 表示 秩为 1,r 是 rank 的简写。这里的秩其实对应的就是上文的 size。
105-
106115如果我们将 0 和 7 进行一次合并。即 ` union(0, 7) ` ,则会发生如下过程。
107116
117+ - 找到 0 的根节点 3
118+ - 找到 7 的根节点 6
119+ - 将 6 指向 3。(为了使得合并之后的树尽可能平衡,一般选择将小树挂载到大树上面,下面的代码模板会体现这一点。3 的秩比 6 的秩大,这样更利于树的平衡,避免出现极端的情况)
120+
108121![ ] ( https://tva1.sinaimg.cn/large/008eGmZEly1gmm4btv06yg30ni05wwze.gif )
109122
110123代码:
@@ -115,6 +128,8 @@ def union(self, p, q):
115128 self .parent[self .find(p)] = self .find(q)
116129```
117130
131+ 这里我并没有判断秩的大小关系,目的是方便大家理清主脉络。完整代码见下面代码区。
132+
118133## 不带权并查集
119134
120135平时做题过程,遇到的更多的是不带权的并查集。相比于带权并查集, 其实现过程也更加简单。
@@ -156,7 +171,7 @@ class UF:
156171
157172## 带权并查集
158173
159- 实际上并查集就是图结构,我们使用了哈希表来模拟这种图的关系。 而上面讲到的其实都是有向无权图 ,因此仅仅使用 parent 表示节点关系就可以了。而如果使用的是有向带权图呢?实际上除了维护 parent 这样的节点指向关系,我们还需要维护节点的权重,一个简单的想法是使用另外一个哈希表 weight 存储节点的权重关系。比如 ` weight[a] = 1 表示 a 到其父节点的权重是 1 ` 。
174+ 上面讲到的其实都是有向无权图 ,因此仅仅使用 parent 表示节点关系就可以了。而如果使用的是有向带权图呢?实际上除了维护 parent 这样的节点指向关系,我们还需要维护节点的权重,一个简单的想法是使用另外一个哈希表 weight 存储节点的权重关系。比如 ` weight[a] = 1 表示 a 到其父节点的权重是 1 ` 。
160175
161176如果是带权的并查集,其查询过程的路径压缩以及合并过程会略有不同,因为我们不仅关心节点指向的变更,也关心权重如何更新。比如:
162177
@@ -246,9 +261,22 @@ return True
246261
247262- [ 684. 冗余连接] ( https://leetcode-cn.com/problems/redundant-connection/solution/bing-cha-ji-mo-ban-ben-zhi-jiu-shi-jian-0wz2m/ )
248263- [ Forest Detection] ( https://binarysearch.com/problems/Forest-Detection )
249-
250264- 最小生成树经典算法 Kruskal
251265
266+ ## 练习
267+
268+ 关于并查集的题目不少,官方给的数据是 30 道(截止 2020-02-20),但是有一些题目虽然官方没有贴` 并查集 ` 标签,但是使用并查集来说确非常简单。这类题目如果掌握模板,那么刷这种题会非常快,并且犯错的概率会大大降低,这就是模板的好处。
269+
270+ 我这里总结了几道并查集的题目:
271+
272+ - [ 547. 朋友圈] ( ../problems/547.friend-circles.md )
273+ - [ 721. 账户合并] ( https://leetcode-cn.com/problems/accounts-merge/solution/mo-ban-ti-bing-cha-ji-python3-by-fe-lucifer-3/ )
274+ - [ 990. 等式方程的可满足性] ( https://github.com/azl397985856/leetcode/issues/304 )
275+ - [ 1202. 交换字符串中的元素] ( https://leetcode-cn.com/problems/smallest-string-with-swaps/ )
276+ - [ 1697. 检查边长度限制的路径是否存在] ( https://leetcode-cn.com/problems/checking-existence-of-edge-length-limited-paths/ )
277+
278+ 上面的题目前面四道都是无权图的连通性问题,第五道题是带权图的连通性问题。两种类型大家都要会,上面的题目关键字都是** 连通性** ,代码都是套模板。看完这里的内容,建议拿上面的题目练下手,检测一下学习成果。
279+
252280## 总结
253281
254282如果题目有连通,等价的关系,那么你就可以考虑并查集,另外使用并查集的时候要注意路径压缩,否则随着树的高度增加复杂度会逐渐增大。
0 commit comments