并查集
💡 核心思想
并查集(Disjoint Set Union, DSU)用于维护不相交集合的合并与查询。核心操作:find(查找根/代表元素)和 unite/merge(合并两个集合)。加上路径压缩和按秩合并后,每次操作接近 $O(1)$。
并查集是竞赛中最高频的数据结构之一,Kruskal 最小生成树、等价类划分、连通性判断都离不开它。核心代码只有几行:find 带路径压缩,unite 按秩合并。记住:"认祖先找代表"——find 的本质是找到集合的代表元素。
🎯 直觉理解
并查集解决"两个元素是否在同一集合"和"合并两个集合",两种操作都近似 $O(1)$。它就像班级里的"认亲":每个人记住自己的"组长",问两个人是不是一伙的,就顺着组长链往上找,看最终的老大是不是同一个。
两个关键优化:路径压缩(找完老大后,把沿途所有人的组长直接改成老大,下次查找一步到位)和按秩合并(矮树接高树,防止链退化)。
经典应用:判连通块(图里有多少个连通分量)、Kruskal 判环、带权并查集解决"关系传递"类问题(如食物链)。实现只有三个函数:find、union、init。
📝 算法流程
- 初始化:每个元素是自己的集合
- find:沿 parent 向上找根,路径压缩
- unite:将两个集合的根合并
- 可维护集合大小、到根的距离等附加信息
$$\text{路径压缩 + 按秩合并:} O(\alpha(n)) \approx O(1)$$
$$\alpha \text{ 是反阿克曼函数,增长极慢}$$
📊 复杂度分析
| 指标 | 复杂度 |
|---|---|
| 时间 | $O(\alpha(n))$ 每次操作(接近 $O(1)$) |
| 空间 | $O(n)$ |
💻 参考实现(C++)
C++ (C++17)
#include
using namespace std;
int fa[100005], rnk[100005];
int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
void unite(int x, int y) {
x = find(x); y = find(y);
if (x == y) return;
if (rnk[x] < rnk[y]) swap(x, y);
fa[y] = x;
if (rnk[x] == rnk[y]) rnk[x]++;
}
int main() {
int n, m; cin >> n >> m;
iota(fa, fa+n+1, 0);
memset(rnk, 0, sizeof(rnk));
while (m--) {
int op, x, y; cin >> op >> x >> y;
if (op == 1) unite(x, y);
else cout << (find(x)==find(y)?"Y":"N") << "\n";
}
return 0;
} ⚠️ 常见坑点
路径压缩只写 return find(fa[x]) 忘记赋值回 fa[x]
按秩合并用深度而非子树大小(两者都可以但含义不同)
初始化忘记 iota/fa[i]=i
带权并查集的距离维护容易出错
📚 相关题目
| 题目 | 来源 | 难度 | 备注 |
|---|---|---|---|
| P3367 并查集 | 洛谷 | CSP-S | 并查集模板 |
| P1551 亲戚 | 洛谷 | CSP-J | 并查集入门 |