引入
在修建道路时,为了让尽可能多的点连通,需要修建连通两个点的公路,这就需要随时询问两个点是否已经连通。
若将已经连通的点看作一个集合,那么修建一条公路的意义,就是合并两个集合,所以如何快速查询两个点是否属于同一个集合,以及快速地合并两个集合,十分重要。
集合
集合与并查集的概念
集合是由一个或多个确定的元素所构成的整体。集合中的元素有如下 三个特征:
-
确定性:一个元素要么属于集合,要么不属于集合。
-
互异性:集合中的元素互不相同。
-
无序性:集合中的元素没有先后顺序。
并查集是一个可以维护集合的数据结构,它能高效支持集合的基本操作:
- 合并两个集合。
- 查询两个指定元素是否属于同一个集合。
需要注意的是,由于计算机存储结构的限制,并查集维护的集合是离散意义下的集合,而不是广义的集合。集合中的元素是有限的。
集合的存储
-
数组存储
存储一个集合最简单的方式就是直接用数组。我们将一个集合的所有元素按某种特定顺序存储在数组里。使用数组存储集合,可以支持较丰富的集合操作,但是维护集合的时间复杂度较高,对于几乎所有操作,单次操作的时间复杂度都是和集合大小成正比的。在C++语言中, 我们可以使用STL中的vector来实现数组存储集合。
-
链表存储
可以模仿数组存储方式,将集合中的元素存储在一个链表里。链表一大好处是可以避免元素的复制,这对于合并操作是比较有帮助的,在定位元素所在的集合的两端后,直接将两个集合的端点相接即可合并完成。然而,使用链表维护集合的最坏情况时间复杂度仍然是与集合大小成正比的,在时间效率上还不够优秀。
在 C++ 语言中,我们可以使用 STL 中的 list 来实现链表存储集合。如果题目对时间效率的要求较高,也可以选择自行实现链表。
值得一提的是,假设元素总数是 nn,且仅需要支持合并和查询操作,那 么上述两种方式可以采用启发式合并的技术(即每次将较小集合合并入较大集合并修改较小集合所有元素的信息)做到总时间复杂度 O(n\log_2 n)O(nlog2n)。虽然 单次操作的时间复杂度较高,但是可以证明总时间复杂度是可以接受的。
-
森林存储
这就是并查集。
并查集
因为一个元素只可能属于一个集合,所以我们可以为每一个集合选取一个代表元。于是查询两个元素是否属于同一个集合实际上就是询问两个元素所在集合的代表元是否相同。这个询问的时间复杂度可以利用数组标记优化为 O(1)O(1)。
但是合并两个集合时需要改变其中一个集合中所有元素的代表元,时间复杂度仍然非常高,如何优化呢?
注意到,合并操作的时间复杂度远高于查询操作的复杂度,这启发我们通过一定的方式,提高查询操作的复杂度,降低合并操作的复杂度。
我们并不需要 O(1)O(1) 知道每个元素所属集合的代表元,这启发我们用森林来维护代表元。用森林中的一棵树代表一个集合,树根为对应集合的代表元。
这样,对于每棵树上的元素,查询其代表元时,时间复杂度与树的高度成正比。
对两个集合进行合并操作时,只需将其中一个集合的代表元(树根)指向另一个集合(树)的代表元即可。时间复杂度也与树的高度成正比。
这就是并查集。
并查集是一种树形的数据结构,顾名思义,它用于处理一些不交集的 合并 及 查询 问题。它支持两种操作:
- 查找(Find):确定某个元素处于哪个子集;
- 合并(Union):将两个子集合并成一个集合。