GESP 7级
并查集
7级 · DP/图论/搜索
📖 一句话理解
并查集(Union-Find)是一种处理"集合合并"和"查询是否在同一集合"的数据结构。想象一群人,有的是朋友。并查集能快速回答:"A和B是朋友吗?"以及"A和B做朋友"(合并两个朋友圈)。
💡 为什么重要?
并查集是最优雅的数据结构之一,代码短小但功能强大。它广泛用于:最小生成树(Kruskal算法)、判断图的连通性、社交网络的好友关系、动态连通性问题等。GESP 7级考试中,并查集是必考内容。
📋 前置知识
- 一维数组:int fa[N] 的使用
- 递归:函数调用自己(find函数会用到递归)
- 了解 图的基本概念 会有帮助
🧠 并查集的核心思想
🔑 每个集合是一棵"树"
并查集用数组 fa[] 来维护集合关系:
fa[x] = y表示 "x的父节点是y"fa[x] = x表示 "x是自己所在集合的根(代表元)"- 从任意节点往上找,最终找到的根就是该集合的代表元
如果两个节点的根相同,它们就在同一集合中。
两个核心操作
📐 find和merge
find(x):找x所在集合的根(代表元)
merge(x, y):把x和y所在的集合合并成一个
判断是否同集合:find(x) == find(y)
merge(x, y):把x和y所在的集合合并成一个
判断是否同集合:find(x) == find(y)
🔢 逐步推演
初始5个独立集合:{1}, {2}, {3}, {4}, {5}
📊 操作过程
初始:fa[1]=1, fa[2]=2, fa[3]=3, fa[4]=4, fa[5]=5
merge(1,2):1的根是1,2的根是2,fa[1]=2
集合变为:{1→2}, {3}, {4}, {5}
merge(3,4):fa[3]=4
集合变为:{1→2}, {3→4}, {5}
merge(2,5):2的根是2,5的根是5,fa[2]=5
集合变为:{1→2→5}, {3→4}, {}
find(1):1→2→5,根是5
find(5):根是5
find(1) == find(5) → 同一集合 ✓
📐 路径压缩(关键优化)
💡 什么是路径压缩?
如果不优化,find(x)可能要沿着链一路往上找,最坏O(n)。
路径压缩:在find的过程中,把沿途所有节点直接连到根上。这样下次查找就是O(1)。
代码只有一行变化:fa[x] = find(fa[x])
💻 并查集完整代码(逐行注释)
#include <iostream>
using namespace std;
const int N = 105;
int fa[N]; // fa[x] = x的父节点
// 查找x所在集合的根(带路径压缩)
int find(int x) {
if (fa[x] == x) // 如果x就是根
return x; // 返回根
return fa[x] = find(fa[x]); // ⭐ 路径压缩:递归找根,同时把x直接连到根
// 等价于:fa[x] = find(fa[x]); return fa[x];
}
// 合并x和y所在的集合
void merge(int x, int y) {
fa[find(x)] = find(y); // 把x的根连到y的根上
}
int main() {
int n; // 节点数
cin >> n;
// 初始化:每个节点自成一个集合
for (int i = 1; i <= n; i++)
fa[i] = i; // 每个节点的父节点是自己(自己是根)
// 示例:合并操作
merge(1, 2); // 合并1和2
merge(3, 4); // 合并3和4
merge(2, 4); // 合并2和4 → 现在1,2,3,4都在同一集合
// 查询:1和3是否在同一集合?
if (find(1) == find(3))
cout << "1和3在同一集合" << endl;
else
cout << "1和3不在同一集合" << endl;
return 0;
}📊 路径压缩的效果
📝 压缩前 vs 压缩后
压缩前(链状结构):
5 ← 4 ← 3 ← 2 ← 1
find(1) 要找4次才能到根
压缩后(扁平结构):
5 ← 1, 2, 3, 4(全部直接连到5)
find(1) 只要1次就能到根!
加上路径压缩,并查集的每个操作几乎是 O(1)(精确说是反阿克曼函数,实际中常数很小)。
🎯 经典应用
应用1:Kruskal最小生成树
💡 思路
Kruskal算法按边权从小到大排序,依次加入不会形成环的边。
怎么判断"加入这条边是否形成环"? 用并查集!如果两个端点已经在同一集合中,加入这条边就会形成环,跳过。
怎么判断"加入这条边是否形成环"? 用并查集!如果两个端点已经在同一集合中,加入这条边就会形成环,跳过。
应用2:朋友圈问题
给定n个人和m对好友关系,问有多少个朋友圈。
💻 朋友圈计数代码
// 输入n个人,m对好友关系,输出朋友圈个数
int main() {
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++) fa[i] = i; // 初始化
for (int i = 0; i < m; i++) {
int a, b;
cin >> a >> b;
merge(a, b); // 合并好友
}
// 统计有多少个不同的根(即朋友圈个数)
int cnt = 0;
for (int i = 1; i <= n; i++) {
if (fa[i] == i) // 如果i是根
cnt++; // 这是一个独立的朋友圈
}
cout << cnt << endl; // 输出朋友圈个数
}🚨 易错点
⚠️ 常见错误汇总
- 忘记路径压缩:find函数中没有
fa[x] = find(fa[x]),效率会很差。一定要写路径压缩! - merge时找根:
fa[find(x)] = find(y),不是fa[x] = y!必须先找根再合并。 - 初始化不能忘:必须先
fa[i] = i初始化,否则 fa 数组是随机值。 - 节点编号范围:看清题目是0-based还是1-based,初始化循环要对应。
- 统计连通分量:数
fa[i] == i的个数,不是数fa[i] != i的个数。 - 按秩合并:更高级的优化是按集合大小合并(小树连到大树),和路径压缩一起用接近O(1)。考试中只写路径压缩通常就够用。
🎯 练习建议
📝 循序渐进练习路径
- 先背模板:并查集的代码非常短,把 find 和 merge 背下来。
- 手动模拟:在纸上画出fa数组的变化过程。
- 做经典题目:
- P3367 【模板】并查集(洛谷模板题)
- P1525 关押罪犯(并查集 + 二分)
- P1111 修复公路(并查集 + 倒序处理)
- 学习Kruskal:并查集 + 贪心 = Kruskal最小生成树算法。