路径压缩的核心是让所有被访问节点直连根,必须在find函数中实现父指针更新,不可依赖union补救;数组需显式初始化并严格对齐索引;压缩与按大小合并须协同,各司其职。
路径压缩的核心不是“找根”,而是“让所有被访问的节点直连根”。只靠数组存父节点远远不够,关键在 find 函数如何利用该数组完成回溯更新或显式重连。
路径压缩必须写进 find,不能靠 union 补救
很多人误以为在 union 里调用 find 就自动压缩了,其实不然——只有真正执行到 find 内部的赋值逻辑,路径才被压平。常见错误是:
- 写成
return find(parent[x]):只递归没改父指针,等于白查 - 在 union 中直接比较
parent[a]和parent[b]:跳过 find,路径完全没动 - 递归 find 写成两行:
int r = find(parent[x]); parent[x] = r; return r;—— 看似等价,但可能被编译器优化掉中间状态,且易漏返回
正确做法是把压缩动作和返回合并为一句:return parent[x] = find(parent[x])(递归)或用迭代两次遍历(先定根、再改父)。
数组初始化必须全量、显式、对齐索引
parent 数组不是声明完就可用,越界或未初始化会导致静默错误或崩溃:
- 若节点编号是 1~n,推荐申请
vector<int> parent(n + 1)</int>,然后iota(parent.begin(), parent.end(), 0) - 避免用
vector<int>(n)</int>却从下标 1 开始访问——parent[n]会越界 - rank 或 size 数组也必须同步初始化:
rank.assign(n + 1, 0)或size.assign(n + 1, 1) - 不初始化 rank 可能导致某些平台读到随机值,union 时挂错方向,树高失控
压缩与按秩/按大小合并要配合,但职责分明
路径压缩负责“查得快”,按秩(或按大小)合并负责“树不高”,两者缺一不可:
- 只压缩不合并:极端数据下树仍可能退化为长链,多次 find 后才压平,首查慢
- 只合并不压缩:每次 find 都要走完整路径,O(log n) 不可避免
- 推荐优先用 按大小合并(union by size):逻辑直观,
size数组天然支持统计连通块节点数,且无需额外维护秩语义 - 合并时务必先
find(x)和find(y),拿到压缩后的根再比大小;否则挂的是非根节点,结构立即损坏
实战中变量命名与结构要防混淆
多个辅助数组共存时,命名和更新必须严格对应:
- 别把
rank当size用,也别把size当高度判断依据 - 路径压缩 不修改 rank 或 size:压缩只改 parent,rank 仅用于 union 决策,size 在 union 成功后才累加
- 若需节省空间,可用
struct Node { int parent; uint8_t rank; }打包进 vector,提升缓存命中率 - 调试时打印某节点路径,不要只看
parent[x],而要一路while (x != parent[x]) x = parent[x]—— 这才是真实链长










