1
1
# 并查集
2
2
3
- 关于并查集的题目不少,官方给的数据是 30 道(截止 2020-02-20),但是有一些题目虽然官方没有贴 ` 并查集 ` 标签,但是使用并查集来说确非常简单。这类题目如果掌握模板,那么刷这种题会非常快,并且犯错的概率会大大降低,这就是模板的好处。
3
+ ## 背景
4
4
5
- 我这里总结了几道并查集的题目:
5
+ 相信大家都玩过下面的迷宫游戏。你的目标是从地图的某一个角落移动到地图的出口。规则很简单,仅仅你不能穿过墙。
6
6
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 )
12
8
13
- 上面的题目前面四道都是无权图的连通性问题,第五道题是带权图的连通性问题。两种类型大家都要会,上面的题目关键字都是** 连通性** ,代码都是套模板。看完这里的内容,建议拿上面的题目练下手,检测一下学习成果。
9
+ 实际上,这道题并不能够使用并查集来解决。 不过如果我将规则变成,“是否存在一条从入口到出口的路径”,那么这就是一个简单的联通问题,只不过用肉眼去观察实在是太费力了。幸好,我们有计算器可以帮助我们。并查集算法就可以用来解决这个问题,并且比暴力枚举效率高。如果地图不变,而不断改变入口和出口的位置,并查集的效果高的超出你的想象。
10
+
11
+ 另外并查集还可以在人工智能中用作图像人脸识别等,感兴趣的可以查找一下相关资料。
14
12
15
13
## 概述
16
14
17
- 并查集是一种树型的数据结构 ,用于处理一些不交集(Disjoint Sets)的合并及查询问题。有一个联合-查找算法(Union-find Algorithm)定义了两个用于此数据结构的操作:
15
+ 并查集使用的是一种 ** 树型 ** 的数据结构 ,用于处理一些不交集(Disjoint Sets)的合并及查询问题。比如让你求两个人是否间接认识,两个地点之间是否有至少一条路径。上面的例子其实都可以抽象为联通性问题。即如果两个点联通,那么这两个点就有至少一条路径能够将其连接起来。值得注意的是,并查集只能回答“联通与否”,而不能回答诸如“具体的联通路径是什么”。如果要回答“具体的联通路径是什么”这个问题,则需要借助其他算法,比如广度优先遍历。
18
16
19
- - Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
20
- - Union:将两个子集合并成同一个集合。
17
+ 并查集(Union-find Algorithm)定义了两个用于此数据结构的操作:
21
18
22
- 初始化每一个点都是一个连通域,类似下图:
23
-
24
- ![ ] ( https://tva1.sinaimg.cn/large/008eGmZEly1gmm4f8vpp3j30p9024jra.jpg )
19
+ - Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
25
20
26
- 由于支持这两种操作,一个不相交集也常被称为联合-查找数据结构(Union-find Data Structure)或合并-查找集合(Merge-find Set)。为了更加精确的定义这些方法,需要定义如何表示集合。一种常用的策略是为每个集合选定一个固定的元素,称为代表,以表示整个集合。接着,Find(x) 返回 x 所属集合的代表,而 Union 使用两个集合的代表作为参数 。
21
+ - Union:将两个子集合并成同一个集合 。
27
22
28
23
## 形象解释
29
24
30
25
比如有两个司令。 司令下有若干军长,军长下有若干师长。。。
31
26
32
- 我们如何判断某两个师长是否属于同一个司令呢(连通性)?
27
+ ### 判断两个节点是否联通
28
+
29
+ 我们如何判断某两个师长是否归同一个司令管呢(连通性)?
33
30
34
31
![ ] ( https://tva1.sinaimg.cn/large/007S8ZIlly1ghlufxh5lhj30gs0bzwet.jpg )
35
32
36
- 很简单,我们顺着师长,往上找,找到司令。 如果两个师长找到的是同一个司令,那么就属于同一个司令。我们用 parent [ x ] = y 表示 x 的 parent 是 y,通过不断沿着搜索 parent 搜索找到 root,然后比较 root 是否相同即可得出结论 。
33
+ 很简单,我们顺着师长,往上找,找到司令。 如果两个师长找到的是同一个司令,那么两个人就归同一个司令管 。
37
34
38
- 以上过程涉及了两个基本操作 ` find ` 和 ` connnected ` 。 并查集除了这两个基本操作,还有一个是 ` union ` 。即将两个集合合并为同一个 。
35
+ 代码上我们可以用 parent [ x ] = y 表示 x 的 parent 是 y,通过不断沿着搜索 parent 搜索找到 root,然后比较 root 是否相同即可得出结论。 这里的 root 实际上就是上文提到的 ** 集合代表 ** 。
39
36
40
- > 为了使得合并之后的树尽可能平衡,一般选择将小树挂载到大树上面,之后的代码模板会体现这一点
37
+ 这个不断往上找的操作,我们一般称为 find,使用 ta 我们可以很容易地求出两个节点是否连通。
38
+
39
+ ### 合并两个联通区域
41
40
42
41
如图有两个司令:
43
42
51
50
52
51
## 核心 API
53
52
53
+ 首先我们初始化每一个点都是一个连通域,类似下图:
54
+
55
+ ![ ] ( https://tva1.sinaimg.cn/large/008eGmZEly1gmm4f8vpp3j30p9024jra.jpg )
56
+
57
+ 为了更加精确的定义这些方法,需要定义如何表示集合。一种常用的策略是为每个集合选定一个固定的元素,称为代表,以表示整个集合。接着,Find(x) 返回 x 所属集合的代表,而 Union 使用两个集合的代表作为参数进行合并。
58
+
59
+ > 这里的代表就是上面的“司令”。
60
+
54
61
### find
55
62
56
63
``` python
@@ -70,12 +77,16 @@ def find(self, x):
70
77
return x
71
78
```
72
79
73
- (这里我进行了路径的压缩)
80
+ 这里我在递归实现的 find 过程进行了路径的压缩,每次往上查找之后都会将树的高度降低到 2。
81
+
82
+ 这有什么用呢?我们知道每次 find 都会从当前节点往上不断搜索,直到到达根节点,因此 find 的时间复杂度大致相等于节点的深度,最坏的情况节点的深度等于树的高度,树的高度如果不加控制则可能为节点数,因此 find 的时间复杂度可能会退化到 $O(n)$。而如果进行路径压缩,那么树的平均高度不会超过 $logn$,具体证明略。不过给大家画了一个图来辅助大家理解。
74
83
75
84
比如对于如下的一个图:
76
85
77
86
![ ] ( https://tva1.sinaimg.cn/large/008eGmZEly1gmm4g5jyb9j30og05naaa.jpg )
78
87
88
+ 图中 r:1 表示 秩为 1,r 是 rank 的简写。这里的秩其实对应的就是上文的 size。(下同,不再赘述)
89
+
79
90
调用 find(0) 会逐步找到 3 ,在找到 3 的过程上会将路径上的节点都指向根节点。
80
91
81
92
![ ] ( https://tva1.sinaimg.cn/large/008eGmZEly1gmm4i1vrclg30ni05wtj9.gif )
@@ -101,10 +112,12 @@ def connected(self, p, q):
101
112
102
113
![ ] ( https://tva1.sinaimg.cn/large/008eGmZEly1gmm4avz4iej30lv04rmx9.jpg )
103
114
104
- 图中 r:1 表示 秩为 1,r 是 rank 的简写。这里的秩其实对应的就是上文的 size。
105
-
106
115
如果我们将 0 和 7 进行一次合并。即 ` union(0, 7) ` ,则会发生如下过程。
107
116
117
+ - 找到 0 的根节点 3
118
+ - 找到 7 的根节点 6
119
+ - 将 6 指向 3。(为了使得合并之后的树尽可能平衡,一般选择将小树挂载到大树上面,下面的代码模板会体现这一点。3 的秩比 6 的秩大,这样更利于树的平衡,避免出现极端的情况)
120
+
108
121
![ ] ( https://tva1.sinaimg.cn/large/008eGmZEly1gmm4btv06yg30ni05wwze.gif )
109
122
110
123
代码:
@@ -115,6 +128,8 @@ def union(self, p, q):
115
128
self .parent[self .find(p)] = self .find(q)
116
129
```
117
130
131
+ 这里我并没有判断秩的大小关系,目的是方便大家理清主脉络。完整代码见下面代码区。
132
+
118
133
## 不带权并查集
119
134
120
135
平时做题过程,遇到的更多的是不带权的并查集。相比于带权并查集, 其实现过程也更加简单。
@@ -156,7 +171,7 @@ class UF:
156
171
157
172
## 带权并查集
158
173
159
- 实际上并查集就是图结构,我们使用了哈希表来模拟这种图的关系。 而上面讲到的其实都是有向无权图 ,因此仅仅使用 parent 表示节点关系就可以了。而如果使用的是有向带权图呢?实际上除了维护 parent 这样的节点指向关系,我们还需要维护节点的权重,一个简单的想法是使用另外一个哈希表 weight 存储节点的权重关系。比如 ` weight[a] = 1 表示 a 到其父节点的权重是 1 ` 。
174
+ 上面讲到的其实都是有向无权图 ,因此仅仅使用 parent 表示节点关系就可以了。而如果使用的是有向带权图呢?实际上除了维护 parent 这样的节点指向关系,我们还需要维护节点的权重,一个简单的想法是使用另外一个哈希表 weight 存储节点的权重关系。比如 ` weight[a] = 1 表示 a 到其父节点的权重是 1 ` 。
160
175
161
176
如果是带权的并查集,其查询过程的路径压缩以及合并过程会略有不同,因为我们不仅关心节点指向的变更,也关心权重如何更新。比如:
162
177
@@ -246,9 +261,22 @@ return True
246
261
247
262
- [ 684. 冗余连接] ( https://leetcode-cn.com/problems/redundant-connection/solution/bing-cha-ji-mo-ban-ben-zhi-jiu-shi-jian-0wz2m/ )
248
263
- [ Forest Detection] ( https://binarysearch.com/problems/Forest-Detection )
249
-
250
264
- 最小生成树经典算法 Kruskal
251
265
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
+
252
280
## 总结
253
281
254
282
如果题目有连通,等价的关系,那么你就可以考虑并查集,另外使用并查集的时候要注意路径压缩,否则随着树的高度增加复杂度会逐渐增大。
0 commit comments