树链剖分
问题引入
有时候我们会不可避免地遇上一些复杂的树上问题:
- 修改 树上两点之间的路径上 所有点的值。
- 查询 树上两点之间的路径上 节点权值的 和/极值/其它。
这些问题暴力解决都具有一定难度,但是如果能够有一种方式将树上问题转化为序列上的问题,那么就能够运用相关数据结构在 \(\mathcal{O}(\log n)\) 的时间内处理。
树链剖分便提供了如此方案,能够将整棵树剖分为若干条链,使它组合成线性结构,然后用其他的数据结构维护信息。下面我们主要讨论四种问题的解决(\(\text{Luogu P3384}\)):
- 将树从 \(u\) 到 \(v\) 结点最短路径上所有节点的值都加上 \(val\)。
- 求树从 \(u\) 到 \(v\) 结点最短路径上所有节点的值之和。
- 将以 \(u\) 为根节点的子树内所有节点值都加上 \(val\)。
- 求以 \(u\) 为根节点的子树内所有节点值之和。
需要注意的是,树链剖分本身不能维护任何信息。因此,学习树链剖分前还需熟练使用诸如线段树、树状数组等等常见数据结构以及差分等序列操作思想。
基本概念
树链剖分(树剖 / 链剖)有多种形式,如 重链剖分,长链剖分 和用于 Link/cut Tree 的剖分(有时被称作「实链剖分」),在此,我们讨论的是「重链剖分」。
重儿子 对于每一个非叶子节点,它的儿子中子树规模最大的一个为重儿子。
轻儿子 对于每一个非叶子节点,它的儿子中除了重儿子以外的所有儿子即为轻儿子。
重边 一个父亲连接他的重儿子的边称为重边。
轻边 除了重边以外的所有边即为轻边。
重链 相邻重边连起来组成的一条链叫重链,每一条重链一定以根或轻儿子为起点。

剖分实现
通常使用 2 次 DFS 实现重链剖分。
注意以下变量及其意义:
- \(\texttt{dep}\):节点深度
- \(\texttt{siz}\):子树大小
- \(\texttt{son}\):重儿子编号
- \(\texttt{fa}\):父节点编号
- \(\texttt{top}\):该节点所在重链的顶点
- \(\texttt{id}\):DFS 序(dfn),也是该结点剖分后对应的序列上的序号
- \(\texttt{w1[i]}\):剖分前 \(i\) 号节点对应的点权
- \(\texttt{w[i]}\):剖分后 \(i\) 号节点对应的点权(注意此处 \(i\) 与上一行的 \(i\) 不一定一致)
第一次 DFS
第一次 DFS 不进行剖分,主要是预处理出任意节点 \(u\) 的 深度、子树大小、重儿子编号、父节点编号
具体操作较简单,此处不再展开
void dfs1(int u, int f){
dep[u] = dep[f] + 1, fa[u] = f, siz[u] = 1;
int Max = -1;
for(auto v : G[u]){
if(v == f) continue;
dfs1(v, u); siz[u] += siz[v];
if(siz[v] > Max) son[u] = v, Max = siz[v];
}
return;
}
第二次 DFS
预处理出任意节点 \(u\) 的 所在重链顶点,DFS 序
注意此处为了使剖分后每条重链上的点对应的区间连续,在 DFS 过程中每次都应先从重儿子开始。
void dfs2(int u, int Top){
id[u] = ++cnt, w[cnt] = w1[u], top[u] = Top;
if(son[u]) dfs2(son[u], Top);
for(auto v : G[u]){
if(v == fa[u] || v == son[u]) continue;
dfs2(v, v);
}
return;
}
以上两次 DFS 的时间复杂度都是 \(\mathcal{O}(n)\) 的。
性质初窥
经过两次的 DFS,我们得到的序列具有以下性质:
-
从根节点出发到任意节点不会经超过 \(\boldsymbol{\mathcal{O}(\log n)}\) 条重链
当我们向下经过一条 轻边 时,所在子树的大小至少会除以二
所以我们至少跨过 \(\mathcal{O}(\log n)\) 条轻边,即 \(\mathcal{O}(\log n)\) 条重链
-
树上每个节点都属于且仅属于一条重链。
-
一条重链上的点 \(\texttt{id}\) 连续。
-
一条子树上的点 \(\texttt{id}\) 连续。
简单应用
路径维护
求两点之间的路径相当于求他们的 LCA。我们先分类讨论:
- 若 \(u\) 和 \(v\) 在同一条重链上,那么 LCA 即深度较小的点;
- 否则如果 \(\texttt{dep[u]}\ge\texttt{dep[v]}\),则 \(u\) 跳到重链顶端的父节点上;如果 \(\texttt{dep[v]}\ge\texttt{dep[u]}\),则 \(v\) 跳到重链顶端的父节点上;直至 \(u\) 与 \(v\) 处于同一重链位置。
注意到以上操作和倍增求 LCA 极其相似,我们便可使用相似的代码来处理。
以下提供 路径加 和 路径求和 的参考代码:
void modify_chain(int u, int v, int val){
while(top[u] != top[v]){
if(dep[top[u]] < dep[top[v]]) swap(u, v);
modify(1, id[top[u]], id[u], val);
u = fa[top[u]];
}
if(dep[u] > dep[v]) swap(u, v);
modify(1, id[u], id[v], val);
return;
}
int query_chain(int u, int v){
int ans = 0;
while(top[u] != top[v]){
if(dep[top[u]] < dep[top[v]]) swap(u, v);
add(ans, query(1, id[top[u]], id[u]));
u = fa[top[u]];
}
if(dep[u] > dep[v]) swap(u, v);
add(ans, query(1, id[u], id[v]));
return ans;
}
时间复杂度取决于 modify 和 query 操作的复杂度,一般为 \(\mathcal{O}(n\log n)\),而路径操作需要 \(\mathcal{O}(\log n)\) 的复杂度,因此总复杂度为 \(\mathcal{O}(n\log^2n)\)。
子树维护
子树维护较简单,注意到 DFS 序的连续性即可。
void modify_tree(int u, int val){
modify(1, id[u], id[u] + siz[u] - 1, val);
}
int query_tree(int u){
return query(1, id[u], id[u] + siz[u] - 1);
}
求 LCA
不断向上跳重链,当跳到同一条重链上时,深度较小的结点即为 LCA。
向上跳重链时需要先跳所在重链顶端深度较大的那个。
int LCA(int u, int v) {
while(top[u] != top[v]) {
if(dep[top[u]] > dep[top[v]]) u = fa[top[u]];
else v = fa[top[v]];
}
return dep[u] > dep[v] ? v : u;
}
例题练习
[模板] 重链剖分/树链剖分
模板题。实现树上路径加、子树加。维护树上路径和、子树和。
[HAOI2015] 树上操作
实现单点加、子树加。维护路径和。
[ZJOI2008] 树的统计
实现树上单点修改。维护树上路径和、最大值。

浙公网安备 33010602011771号