4040
4141在使用「快速查询」思路实现并查集时,我们可以使用一个「数组结构」来表示集合中的元素。数组元素和集合元素是一一对应的,我们可以将数组的索引值作为每个元素的集合编号,称为 $id$。然后可以对数组进行以下操作来实现并查集:
4242
43- - ** 当初始化时** :将每个元素的集合编号初始化为数组下标索引。则所有元素的 $id$ 都是唯一的,代表着每个元素单独属于一个集合。
43+ - ** 当初始化时** :将数组下标索引值作为每个元素的集合编号。所有元素的 $id$ 都是唯一的,代表着每个元素单独属于一个集合。
4444- ** 合并操作时** :需要将其中一个集合中的所有元素 $id$ 更改为另一个集合中的 $id$,这样能够保证在合并后一个集合中所有元素的 $id$ 均相同。
4545- ** 查找操作时** :如果两个元素的 $id$ 一样,则说明它们属于同一个集合;如果两个元素的 $id$ 不一样,则说明它们不属于同一个集合。
4646
47- 举个例子来说明一下,我们使用数组来表示一系列集合元素 $\left\{ 0 \right\} , \left\{ 1 \right\} , \left\{ 2 \right\} , \left\{ 3 \right\} , \left\{ 4 \right\} , \left\{ 5 \right\} , \left\{ 6 \right\} , \left\{ 7 \right\} $,初始化时如下图所示。从下图中可以看出:元素的集合编号就是数组的索引值,代表着每个元素属于一个集合。
47+ 举个例子来说明一下,我们使用数组来表示一系列集合元素 $\left\{ 0 \right\} , \left\{ 1 \right\} , \left\{ 2 \right\} , \left\{ 3 \right\} , \left\{ 4 \right\} , \left\{ 5 \right\} , \left\{ 6 \right\} , \left\{ 7 \right\} $,初始化时如下图所示。
4848
49- ![ ] ( ../../images/20220505145234 .png )
49+ ![ 基于数组实现:初始化操作 ] ( ../../images/20240513150949 .png )
5050
51- 当我们进行一系列的合并操作后,比如合并后变为 $\left \{ 0 \right \} , \left \{ 1, 2, 3 \right \} , \left \{ 4 \right \} , \left \{ 5, 6\right \} , \left \{ 7 \right \} $,合并操作的结果如下图所示。从图中可以看出,在进行一系列合并操作后,下标为 $1$、$2$、$3$ 的元素集合编号是一致的,说明这 $3$ 个 元素同属于一个集合。同理下标为 $5$ 和 $6$ 的元素则同属于另一个集合 。
51+ 从上图中可以看出:数组的每个下标索引值对应一个元素的集合编号,代表着每个元素单独属于一个集合 。
5252
53- ![ ] ( ../../images/20220505145302.png )
53+ 当我们进行一系列的合并操作后,比如合并后变为 $\left\{ 0 \right\} , \left\{ 1, 2, 3 \right\} , \left\{ 4 \right\} , \left\{ 5, 6\right\} , \left\{ 7 \right\} $,合并操作的结果如下图所示。
54+
55+ ![ 基于数组实现:合并操作] ( ../../images/20240513151310.png )
56+
57+ 从上图中可以看出,在进行一系列合并操作后,下标为 $1$、$2$、$3$ 的元素集合编号是一致的,说明这 $3$ 个元素同属于一个集合。同理下标为 $5$ 和 $6$ 的元素则同属于另一个集合。
5458
5559在快速查询的实现思路中,单次查询操作的时间复杂度是 $O(1)$,而单次合并操作的时间复杂度为 $O(n)$(每次合并操作需要遍历数组)。两者的时间复杂度相差得比较大,完全牺牲了合并操作的性能。因此,这种并查集的实现思路并不常用。
5660
@@ -94,17 +98,41 @@ class UnionFind:
9498
9599总结一下,我们可以对数组 $fa$ 进行以下操作来实现并查集:
96100
97- - ** 当初始化时** :将每个元素的集合编号初始化为数组 $fa$ 的下标索引。所有元素的根节点的集合编号不一样 ,代表着每个元素单独属于一个集合。
101+ - ** 当初始化时** :将数组 $fa$ 的下标索引作为每个元素的集合编号。所有元素的根节点的集合编号都不一样 ,代表着每个元素单独属于一个集合。
98102- ** 合并操作时** :需要将两个集合的树根节点相连接。即令其中一个集合的树根节点指向另一个集合的树根节点(` fa[root1] = root2 ` ),这样合并后当前集合中的所有元素的树根节点均为同一个。
99103- ** 查找操作时** :分别从两个元素开始,通过数组 $fa$ 存储的值,不断递归访问元素的父节点,直到到达树根节点。如果两个元素的树根节点一样,则说明它们属于同一个集合;如果两个元素的树根节点不一样,则说明它们不属于同一个集合。
100104
101- 举个例子来说明一下,我们使用数组来表示一系列集合元素 $\left\{ 0\right\} , \left\{ 1 \right\} , \left\{ 2 \right\} , \left\{ 3 \right\} , \left\{ 4 \right\} , \left\{ 5 \right\} , \left\{ 6 \right\} , \left\{ 7 \right\} $,初始化时如下图所示。从下图中可以看出:元素的集合编号就是数组 $fa$ 的索引值,代表着每个元素属于一个集合。
105+ 举个例子来说明一下,我们使用数组来表示一系列集合元素 $\left\{ 0\right\} , \left\{ 1 \right\} , \left\{ 2 \right\} , \left\{ 3 \right\} , \left\{ 4 \right\} , \left\{ 5 \right\} , \left\{ 6 \right\} , \left\{ 7 \right\} $,初始化时如下图所示。
106+
107+ ![ 基于森林实现:初始化操作] ( ../../images/20240513151548.png )
108+
109+ 从上图中可以看出:$fa$ 数组的每个下标索引值对应一个元素的集合编号,代表着每个元素属于一个集合。
110+
111+ 当我们进行一系列的合并操作后,比如 ` union(4, 5) ` 、` union(6, 7) ` 、` union(4, 7) ` 操作后变为 $\left\{ 0 \right\} , \left\{ 1 \right\} , \left\{ 2 \right\} , \left\{ 3 \right\} , \left\{ 4, 5, 6, 7 \right\} $,合并操作的步骤及结果如下图所示。
112+
113+ ::: tabs#union
114+
115+ @tab <1>
116+
117+ - 合并 $(4, 5)$:令 $4$ 的根节点指向 $5$,即将 $fa[ 4] $ 更改为 $5$。
118+
119+ ![ 基于森林实现:合并操作 1] ( ../../images/20240513154015.png )
120+
121+ @tab <2>
122+
123+ - 合并 $(6, 7)$:令 $6$ 的根节点指向 $7$,即将 $fa[ 6] $ 更改为 $7$。
124+
125+ ![ 基于森林实现:合并操作 2] ( ../../images/20240513154022.png )
126+
127+ @tab <3>
128+
129+ - 合并 $(4, 7)$:令 $4$ 的的根节点指向 $7$,即将 $fa[ fa[ 4]] $(也就是 $fa[ 5] $)更改为 $7$。
102130
103- ![ ] ( ../../images/20220507112934 .png )
131+ ![ 基于森林实现:合并操作 3 ] ( ../../images/20240513154030 .png )
104132
105- 当我们进行一系列的合并操作后,比如 ` union(4, 5) ` 、 ` union(6, 7) ` 、 ` union(4, 7) ` 操作后变为 $\left \{ 0 \right \} , \left \{ 1 \right \} , \left \{ 2 \right \} , \left \{ 3 \right \} , \left \{ 4, 5, 6, 7 \right \} $,合并操作的步骤及结果如下图所示。从图中可以看出,在进行一系列合并操作后, ` fa[4] == fa[5] == fa[6] == fa[fa[7]] ` ,即 $4$、$5$、$6$、$7$ 的元素根节点编号都是 $4$,说明这 $4$ 个 元素同属于一个集合。
133+ :::
106134
107- ![ ] ( ../../images/20220507142647.png )
135+ 从上图中可以看出,在进行一系列合并操作后, ` fa[fa[4]] == fa[5] == fa[6] == f[7] ` ,即 $4$、$5$、$6$、$7$ 的元素根节点编号都是 $4$,说明这 $4$ 个元素同属于一个集合。
108136
109137- 使用「快速合并」思路实现并查集代码如下所示:
110138
@@ -134,7 +162,7 @@ class UnionFind:
134162
135163在集合很大或者树很不平衡时,使用上述「快速合并」思路实现并查集的代码效率很差,最坏情况下,树会退化成一条链,单次查询的时间复杂度高达 $O(n)$。并查集的最坏情况如下图所示。
136164
137- ![ ] ( ../../images/20220507172300 .png )
165+ ![ 并查集最坏情况 ] ( ../../images/20240513154732 .png )
138166
139167为了避免出现最坏情况,一个常见的优化方式是「路径压缩」。
140168
@@ -148,7 +176,7 @@ class UnionFind:
148176
149177下面是一个「隔代压缩」的例子。
150178
151- ![ ] ( ../../images/20220509113954 .png )
179+ ![ 路径压缩:隔代压缩 ] ( ../../images/20240513154745 .png )
152180
153181- 隔代压缩的查找代码如下:
154182
@@ -166,7 +194,7 @@ def find(self, x): # 查找元素根节点的集合
166194
167195相比较于「隔代压缩」,「完全压缩」压缩的更加彻底。下面是一个「完全压缩」的例子。
168196
169- ![ ] ( ../../images/20220507174723 .png )
197+ ![ 路径压缩:完全压缩 ] ( ../../images/20240513154759 .png )
170198
171199- 完全压缩的查找代码如下:
172200
@@ -197,7 +225,7 @@ def find(self, x): # 查找元素根节点的集合
197225
198226下面是一个「按深度合并」的例子。
199227
200- ![ ] ( ../../images/20220509094655 .png )
228+ ![ 按秩合并:按深度合并 ] ( ../../images/20240513154814 .png )
201229
202230- 按深度合并的实现代码如下:
203231
@@ -242,7 +270,7 @@ class UnionFind:
242270
243271下面是一个「按大小合并」的例子。
244272
245- ![ ] ( ../../images/20220509094634 .png )
273+ ![ 按秩合并:按大小合并 ] ( ../../images/20240513154835 .png )
246274
247275- 按大小合并的实现代码如下:
248276
0 commit comments