不相交集类 (并查集)
不相交集类是解决等价问题的一种有效的数据结构,其实现简单,可以使用一个简单的数组,而且每种操作只需要常数时间。
等价关系
称关系(relation) R定义在集合 S 上,如果对于 a, b ∈ S 的每一对元素(a,b),aRb 或者为 true 或者为 false。如果 aRb 是true,那么就说 a 与 b 有关系。
等价关系(equivalence relation)是满足下列3个性质的关系 R:
- (自反性)对于所有的 a ∈ S,aRa。
- (对称性)aRb 当且仅当 bRa。
- (传递性)若 aRb 且 bRc,则 aRc。
例如关系 ≤ 不是等价关系,因为其满足自反性、传递性,但不满足对称性。
动态等价性问题
一个元素 a ∈S 的等价类是 S 的一个子集,它包含所有与 a 有(等价)关系的元素。
解决等价问题的思路:输入数据最初是 N 个集合组成的类,每个集合包含一个元素。初始的表述是所有的关系均为 false(自反的关系除外)。每个集合都有一个不同的元素,从而 Si ∩ S j = ∅ 。这使得这些集合不相交(disjoint)。
此时有两种操作允许操作。第一种操作是 find,它返回包含给定元素的集合(即等价类)的名字。第二种操作是添加关系,首先要看是否 a 和 b 已经有关系。这可以通过对 a 和 b 执行 find 并检验它们是否在同一个等价类中来完成。如果不在同一等价类中,则使用求并操作 union。这种操作把含有 a 和 b 的两个等价类合并成一个新的等价类。从集合的角度看,∪ 的结果是建立一个新集合 Sk = Si∪Sj,去掉原来两个集合,同时保持所有的集合的不相交性。由于这个原因,常常把做这项工作的算法叫作不相交集合的 union/find 算法(disjoint set union/find slgorithm)。
关键:find(a) == find(b) 为 true 当且仅当 a 和 b 在同一个集合中。如果还要跟踪每个等价类的大小,并在执行union时将较小的等价类的名字改为较大的等价类的名字。
基本数据结构
由于只需要父节点的名字,因此可以假设这棵树被非显式地存储在一个数组中:数组的每个成员 s[i] 表示元素 i 的父亲。如果 i 是根,那么 s[i] = -1。
灵巧求并算法
按大小求并
上面 union 的实施是相当任意的,它通过使用第二棵树成为第一棵树的子树而完成合并。对其进行简单改进,使得总让较小的树成为较大的树的子树。我们把这种方法叫作按大小求并(union by size)。对于图4,假如下一次运算是union(3, 4),那么结果将形成图6 所示的森林。倘若没有对大小进行探测而直接union,那么结果将形成更深的树,如图7。
可以证明,如果这些 union 都是按照大小进行的,那么任何节点的深度均不会超过 logN。
按高度求并
另外一种实现方法为按高度求并(union-by-height),它同样保证所有的树的深度最多也是 O(logN)。我们跟踪每棵树的高度而不是大小,并执行那些 union 使得浅的树成为深的树的子树。这是一种平缓的算法,因为只有当两棵相等深度的树求并时树的高度才增加(此时树的高度增1)。这样,按高度求并是按大小求并的简单修改。由于零的高度不是负的,因此我们实际上存储高度的负值,减去一个附加的1,并且初始时所有的项都是 -1。
/** * 合并两个不相交集合 * 为简明起见,假设root1和root2是互异的 * 并各代表一个集合的名字 * root1为集合1的根 * root2为集合2的根 */ void DisjSets::unionSets(int root1, int root2) { if (s[root2] < s[root1]) //root2更深 s[root1] = root2; //使root2为新的根 else { if (s[root1] == s[root2]) --s[root1]; //如果相同,则更新高度 s[root2] = root1; //使root1为新的根 } }
路径压缩
路径压缩(path compression)在一次 find 操作期间执行而与用来执行 union 的方法无关。设操作为 find(x),此时路径压缩的效果是,从 x 到根的路径上的每一个节点都使它的父节点变成根。 进行路径压缩时,连续 M 次运算最多需要 O(M logN) 的时间。
/** * 利用路径压缩执行一次find * 简单起见,忽略差错检验 * 返回包含x的集合 */ int DisjSets::find(int x) { if (s[x] < 0) return x; else return s[x] = find(s[x]); }
实现代码
class DisjSets { public: /** * 构造不相交集对象 * numElements为不相交集的初始个数 */ explicit DisjSets(int numElements):s{ numElements,-1 } { } /** * 执行一次find * 为简单起见,再次忽略差错检验 * 返回包含x的集合 */ int find(int x)const { if (s[x] < 0) return x; else return find(s[x]); } int find(int x) { if (s[x] < 0) return x; else return find(s[x]); } /** * 合并两个不相交集合 * 为简明起见,假设root1和root2是互异的 * 并各代表一个集合的名字 * root1为集合1的根 * root2为集合2的根 */ void unionSets(int root1, int root2) { s[root2] = root1; } private: std::vector<int> s; };
一个应用
一个应用 union/find 数据结构的例子就是迷宫的生成。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!