并查集
💡 核心思想
并查集(Disjoint Set Union, DSU)用于维护不相交集合的合并与查询。核心操作:find(查找根/代表元素)和 unite/merge(合并两个集合)。加上路径压缩和按秩合并后,每次操作接近 $O(1)$。
并查集是竞赛中最高频的数据结构之一,Kruskal 最小生成树、等价类划分、连通性判断都离不开它。核心代码只有几行:find 带路径压缩,unite 按秩合并。记住:"认祖先找代表"——find 的本质是找到集合的代表元素。
🎯 直觉理解
并查集就像"帮派"系统。每个人有一个"老大",老大就是帮派的代表。你要知道两个人是否在同一个帮派,就看他们的"最终老大"是不是同一个人。路径压缩就是:不管中间经过多少层,直接告诉每个人他的最终老大是谁,以后查得更快。
📝 算法流程
- 初始化:每个元素是自己的集合
- find:沿 parent 向上找根,路径压缩
- unite:将两个集合的根合并
- 可维护集合大小、到根的距离等附加信息
$$\text{路径压缩 + 按秩合并:} O(\alpha(n)) \approx O(1)$$
$$\alpha \text{ 是反阿克曼函数,增长极慢}$$
📊 复杂度分析
| 指标 | 复杂度 |
|---|---|
| 时间 | $O(\alpha(n))$ 每次操作(接近 $O(1)$) |
| 空间 | $O(n)$ |
💻 参考实现(C++)
C++ (C++17)
#include <bits/stdc++.h>
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 | 并查集入门 |