原始题目
下面并查集代码的横线处应该填什么?
int parent[N];
int find(int x) {
if (parent[x] != x) {
parent[x] = ___;
}
return parent[x];
}
- A.
find(parent[x]) - B.
parent[find(x)] - C.
x - D.
parent[x]
答案:A(
find(parent[x]))
这就是并查集查找操作的路径压缩标准写法:
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // 递归找到根,顺手把 x 直接挂到根上
}
return parent[x];
}
图解:压缩前 vs 压缩后
原理:parent[x] != x 说明 x 不是根。递归 find(parent[x]) 一路找到根,返回时把每个经过的节点直接改指向根——下次再查这条链,一步到位。
逐项排除
| 选项 | 判断 | 错在哪 |
|---|---|---|
A. find(parent[x]) |
✅ | 先找到根,再让 x 直接挂到根上——路径压缩 |
B. parent[find(x)] |
❌ | 无限递归:find(x) 还没算出来就要用它自己,直接栈溢出 |
C. x |
❌ | 把 x 自封为根,等于随机拆树,集合结构全乱 |
D. parent[x] |
❌ | 原样赋回自己,空操作,只是普通查找没有压缩 |
配套:union(按秩合并)
void unite(int x, int y) {
int rx = find(x), ry = find(y);
if (rx == ry) return;
if (rk[rx] < rk[ry]) swap(rx, ry);
parent[ry] = rx; // 矮树挂到高树下
if (rk[rx] == rk[ry]) rk[rx]++;
}
两个优化叠加才达到最优:
| 优化组合 | 单次操作均摊复杂度 |
|---|---|
| 都不做 | \(O(n)\)(退化成链) |
| 只用按秩合并 | \(O(\log n)\) |
| 只用路径压缩 | \(O(\log n)\) |
| 两者都用 | \(O(\alpha(n))\) |
\(\alpha(n)\) 是阿克曼反函数,实际应用中不超过 4,可视为常数。
易错细节
- 初始化不能漏:
for (int i = 0; i < n; i++) parent[i] = i;,每个点先自成一树。 - 判同集合用
find(a) == find(b),不能写parent[a] == parent[b]——压缩前parent不一定是根。 - 合并只改根的父亲:
parent[ry] = rx,不要直接改parent[x]或parent[y]。 - 递归版 find 在极端深链(未压缩时)可能栈溢出,工程上可写非递归版:
int find(int x) {
int r = x;
while (parent[r] != r) r = parent[r]; // 先找根
while (parent[x] != x) { // 再统一压缩
int t = parent[x];
parent[x] = r;
x = t;
}
return r;
}
考点总结(CSP-J/S 初赛、程序填空)
- 填空见到
parent[x] != x→ 紧跟parent[x] = find(parent[x]);,这就是路径压缩。 - 路径压缩 + 按秩合并 = \(O(\alpha(n))\),只做其一是 \(O(\log n)\)。
- 判连通 / 最小生成树 Kruskal 都靠它,核心操作只有
find和unite两个。 - 常见坑:忘记初始化、用
parent[直接比较、合并时没先find。
思考题
如果只写路径压缩、不做按秩合并(union 时随便挂),单次操作的最坏均摊复杂度是多少?
答案
\(O(\log n)\),不是 \(\alpha(n)\)。经典结论:只有路径压缩 + 按秩/大小合并两者叠加,才能把均摊复杂度压到反阿克曼级别。单独使用路径压缩时,存在构造出的操作序列使代价达到 \(\Theta(\log n)\)。

![BFS 为什么能求最短路?—— dist[v] = dist[u] + 1 与分层思想](https://gesp-img.oss-accelerate.aliyuncs.com/uploads/202609/13/199d2ea51279b83f.png)

