核心概述
重链剖分(Heavy-Light Decomposition, HLD)将树分解为若干条”重链”与”轻边”,使任意两点路径可被 O(log N) 段链覆盖;结合线段树/树状数组可在 O(log² N) 内支持路径/子树查询与修改。
原文引述
Description: Heavy-Light Decomposition on trees. Decomposes any u–v path into O(log N) chains so path/subtree queries can be answered with a segment tree/fenwick over a base array. Time: Build O(N); per path op O(log² N) with a standard segment tree (can be O(log N) for some ops).
展开阐述
定义与背景
重链剖分是一种树分解技术,将树的边划分为”重边”和”轻边”两类,使任意根到节点的路径经过的轻边数量不超过 O(log N)。该技术最初用于优化树上路径查询,现已成为处理树上区间问题的标准工具。
典型适用场景:
- 树上路径查询与修改(如路径和、路径最大值、路径异或和)
- 子树查询与修改(利用 DFS 序的连续性)
- 树上 LCA 查询(作为副产品)
- 需要将树上问题转化为序列问题的场景
接口/字段/数据结构
核心数组:
parent[u]:节点 u 的父节点depth[u]:节点 u 的深度(根为 0)sz[u]:以 u 为根的子树大小heavy[u]:u 的重儿子编号(若无则为 -1)head[u]:u 所在重链的顶端节点pos[u]:u 在线段树 base 数组中的位置(DFS 时间戳)base[i]:位置 i 对应节点的权值(可选,视需求而定)
典型接口:
HLD(n):构造 n 个节点的结构addEdge(u, v):添加无向边init(root=0):执行两次 DFS 完成预处理lca(u, v):返回最近公共祖先queryPath(u, v):查询路径 u→v 的聚合值updatePath(u, v, val):更新路径 u→vquerySubtree(u):查询以 u 为根的子树updateSubtree(u, val):更新以 u 为根的子树
核心流程/要点
第一次 DFS(计算子树大小与重儿子):
- 从根开始 DFS,计算每个节点的
sz、depth、parent - 对每个节点,选择子树大小最大的儿子作为重儿子
heavy[u] - 父节点到重儿子的边称为”重边”,其余为”轻边”
第二次 DFS(链分解与位置分配):
- 优先沿重边递归,使同一条重链上的节点在 DFS 序中连续
- 为每个节点分配
pos[u](递增的时间戳) - 记录每条链的顶端
head[u]:- 若 u 是重儿子,则
head[u] = head[parent[u]] - 否则
head[u] = u(新链的起点)
- 若 u 是重儿子,则
路径查询/修改(u→v 路径分解):
- 将 u 和 v 分别跳到所在链的顶端,每次处理较深链:
- 设
hu = head[u],hv = head[v] - 若
depth[hu] < depth[hv],交换 u、v - 在线段树上查询/更新区间
[pos[hu], pos[u]] - 令
u = parent[hu](跳到上一条链)
- 设
- 当 u、v 位于同一链时(
head[u] == head[v]),处理最后一段[pos[v], pos[u]](或[pos[u], pos[v]],需保证 u 更深)
子树查询/修改:
- 由于 DFS 序的性质,以 u 为根的子树对应连续区间
[pos[u], pos[u] + sz[u] - 1] - 直接在线段树上对该区间进行单次操作
LCA 查询:
- 在路径跳跃过程中,当
head[u] == head[v]时,深度较小者即为 LCA - 或直接返回最后跳到同一链时深度较小的节点
复杂度与边界条件
时间复杂度:
- 预处理(两次 DFS):O(N)
- 路径操作:O(log N) 段链 × O(log N) 线段树单次操作 = O(log² N)
- 子树操作:O(log N)(单次线段树区间操作)
- LCA 查询:O(log N)
空间复杂度:O(N) 存储数组 + O(N) 线段树
边界条件:
- 单节点树:sz[root]=1, heavy[root]=-1, head[root]=root
- 链状树:退化为一条重链,路径操作仍为 O(log N)
- 完全二叉树:每层轻边数固定,保证 O(log N) 链段数
- 森林:需为每棵树分别执行 init,或添加虚拟根节点
点权 vs 边权:
- 点权:直接在
pos[u]位置存储节点权值 - 边权:将边 (parent[u], u) 的权值存储在
pos[u]位置(较深端点) - 查询路径时,边权版本需注意 LCA 不计入(或根据题意调整)
实现细节与易错点
- DFS 顺序:第二次 DFS 必须优先访问重儿子,确保重链连续
- 链跳方向:始终从较深链向上跳,避免跳过 LCA
- 区间端点:线段树索引需与 pos 数组的基(0-based 或 1-based)保持一致
- 边权处理:若边权映射到子节点,路径查询的最后一段需排除 LCA(或调整查询区间)
- 线段树聚合方向:路径分段的聚合顺序可能影响非交换运算(如矩阵乘法),需按 u→LCA→v 的方向合并
变体与扩展
- 带懒标记的批量更新:结合006-数据结构-懒标记线段树支持路径区间加、区间赋值
- 可持久化重链剖分:结合主席树实现历史版本查询
- 边权聚合:将边权映射到深度较大的端点,查询时注意 LCA 的处理
- LCA 优化:HLD 自带 O(log N) LCA;若需 O(1) LCA 可结合011-数据结构-区间最值查询的 RMQ
- 动态树对比:HLD 适用于静态树;若需动态连边/断边,应使用069-图论-LinkCutTree
正确性要点与不变式
重链性质不变式:
- 从根到任意节点的路径上,轻边数量 ≤ ⌊log₂ N⌋
- 证明:每次经过轻边,进入的子树大小 ≤ 当前子树大小的一半
链分解的完备性:
- 每个节点恰好属于一条重链
- 任意路径可唯一分解为若干链段的并集
DFS 序的连续性:
- 同一重链上的节点在 pos 数组中连续排列
- 子树节点在 pos 数组中形成连续区间
与相邻技术的对比与取舍
HLD vs 054-图论-二进制提升:
- 二进制提升:仅支持 LCA 与祖先跳跃,复杂度 O(log N)
- HLD:支持路径聚合/修改,复杂度 O(log² N),功能更强
HLD vs 069-图论-LinkCutTree:
- HLD:静态树,预处理 O(N),操作 O(log² N),常数小,实现简单
- LCT:动态树(支持 link/cut),操作均摊 O(log N),常数大,实现复杂
- 取舍:静态树优先 HLD;动态树必须 LCT
HLD vs 068-图论-最近公共祖先的欧拉序+RMQ:
- HLD:路径操作强大,LCA 是副产品
- 欧拉序+RMQ:专注于 O(1) LCA 查询
- 取舍:需要路径聚合选 HLD;仅需 LCA 且查询极多选 RMQ
HLD 与线段树的配合:
- 需要区间修改:使用006-数据结构-懒标记线段树
- 仅点修改/区间查询:使用012-数据结构-线段树或003-数据结构-树状数组
- 需要可持久化:使用主席树