← 目录 / 算法文档 · 模块十 数据结构基础 / 10.1 并查集

10.1 并查集

快速回答"这两个元素属于同一组吗"以及"把这两组合并成一组"——并查集用接近 O(1) 的时间支持这两种操作。

本页目录
① 用宿舍分组理解并查集

假设班里 8 个同学要分成若干小组,我们需要快速回答两类问题:

查:"1 号和 5 号是一组吗?"——Find(1) == Find(5) ?

并:"把 3 号所在的组和 7 号所在的组合并成一组。"——Union(3, 7)

并查集的做法是:每个组推选一个代表(根节点),每个人记住自己的上级是谁。要判断两人是否同组,只需看他们各自追溯到的代表是否相同。

组 A(代表:1) 1 根(代表) 2 4 5 3 组 B(代表:6) 6 根(代表) 7 8 Find(3) = 1,Find(7) = 6 Find(2) == Find(5)?→ 1 == 1 ✅ 同组 Find(3) == Find(7)?→ 1 ≠ 6 ❌ 不同组
每棵树是一个分组,树根就是这组的代表。判断两个元素是否同组,只需判断它们的根是否相同。
② parent 数组:每人记住自己的"上级"

并查集只需要一个数组 parent[]parent[i] 存储 i 的上级是谁。根节点的上级是自己:parent[root] == root

初始时每个人都是独立的一组,上级指向自己:

初始化:8 个人,各自一组
下标 i
1
1
2
2
3
3
4
4
5
5
6
6
7
7
8
8
parent[i]
1
2
3
4
5
6
7
8
绿色格子表示根节点(parent[i] == i)。初始时所有人都是自己的根,8 个独立的集合。
③ 查找(Find):找到组长

从 x 出发,沿着 parent 指针一路往上,直到找到根(parent[x] == x 的节点),这就是 x 所在组的代表。

C++ · 朴素 Find(不带路径压缩)
1int Find(int x) {
2 while (parent[x] != x) // 还没到根,继续往上
3 x = parent[x];
4 return x;
5}

问题是:如果树退化成一条链(每次都把新元素挂在最深处),Find 需要 O(n) 时间。下面的路径压缩解决这个问题。

④ 路径压缩:让查找更快

查找根的过程中,顺手把沿途所有节点的 parent 直接指向根。下次再查这条路上的任何节点,一步就到根了。

压缩前(链式,慢) 1 2 3 Find(3) 需要走 2 步 路径压缩 压缩后(扁平,快) 1 2 3 4 所有节点直接挂根,Find 只需 1 步
路径压缩后,树变得非常"扁",之后对这些节点的任何查询都只需一次跳转。加上路径压缩后,Find 的均摊时间复杂度接近 O(1)(严格来说是 O(α(n)),α 是反阿克曼函数,实际上不超过 5)。
C++ · 带路径压缩的 Find(递归写法,一行搞定)
1int Find(int x) {
2 if (parent[x] != x)
3 parent[x] = Find(parent[x]); // 递归找根,顺手把 parent[x] 直接指向根
4 return parent[x];
5}
6
7// 更简洁的一行版本(等价)
8int Find(int x) { return parent[x] == x ? x : parent[x] = Find(parent[x]); }
⑤ 合并(Union):两组合并

合并两个集合,只需找到各自的根,然后让其中一个根的 parent 指向另一个根——两棵树就合并成了一棵。

C++ · Union 操作
1void Union(int x, int y) {
2 int rx = Find(x), ry = Find(y); // 找到各自的根
3 if (rx != ry) // 不同组才需要合并
4 parent[rx] = ry; // 把 rx 的根挂到 ry 下面
5}
6
7// 判断是否同组
8bool Same(int x, int y) { return Find(x) == Find(y); }
💡
按秩合并(可选优化):每次把小树挂到大树的根下,避免树高度增长过快。用 size[] 记录每棵树的节点数,合并时把小的挂到大的上。实际竞赛中,只加路径压缩通常就够用了——两者同时使用可以达到理论最优。
⑥ 完整模板代码
C++ · 并查集完整模板(带路径压缩 + 按大小合并)
1const int MAXN = 100005;
2int parent[MAXN], sz[MAXN];
3
4void init(int n) {
5 for (int i = 1; i <= n; i++)
6 parent[i] = i, sz[i] = 1; // 每人自成一组,大小为 1
7}
8
9int Find(int x) {
10 return parent[x] == x ? x : parent[x] = Find(parent[x]);
11}
12
13void Union(int x, int y) {
14 int rx = Find(x), ry = Find(y);
15 if (rx == ry) return; // 已经同组,不用合并
16 if (sz[rx] < sz[ry]) swap(rx, ry); // 小树挂到大树
17 parent[ry] = rx;
18 sz[rx] += sz[ry];
19}
20
21bool Same(int x, int y) { return Find(x) == Find(y); }
⑦ 竞赛常见应用
题型并查集的用法关键操作
判断连通性 把图的每条边两端 Union,最后查询两点是否 Same Union + Same
统计连通分量数 初始分量数 = n,每次成功 Union 就 −1 Union + 计数
Kruskal 最小生成树 按边权从小到大枚举,若两端不同组则加入 MST 并 Union Sort + Union + Same
朋友圈 / 社交分组 "A 认识 B,B 认识 C"则三人同组,查询任意两人是否认识 Union + Same
判断图是否有环 加边前若两端已 Same,则加这条边会形成环 Same(加边前检查)
📖
本节小结
・并查集维护一个 parent[] 数组,支持合并集合查询是否同集合两种操作。
Find 沿 parent 链找根;加上路径压缩后均摊接近 O(1)。
Union 把两个根连起来;加上按大小合并防止树过高。
・竞赛中的连通性问题、MST(Kruskal)、判环都离不开并查集,模板务必背熟。