一、为什么需要树链剖分?
1.1 问题背景
在树上,我们经常需要处理以下类型的操作:
- 将节点
u到v的路径上的所有点权 +x - 查询节点
u到v的路径上所有点权的最大值 - 将以
u为根的子树所有节点权值 +x - 查询以
u为根的子树的所有节点权值和
这些问题如果用 LCA + 暴力遍历路径,单次操作复杂度为 O(n),效率极低。
1.2 解决思路
树链剖分的核心思想是:
把树“拉直”成一条线,然后用线段树维护这条线,从而将树上路径操作转化为区间操作。
但直接“拉直”会丢失结构信息。树链剖分通过一种有规则的 DFS 序,保证了:任意两点间的路径,最多由 O(log n) 条连续的链组成。
二、核心概念与定义
2.1 重儿子(Heavy Child)
- 对于一个非叶子节点
u,其子树大小最大的子节点称为u的重儿子。 - 如果有多个子树大小相同,任选一个即可。
2.2 轻儿子(Light Child)
- 除了重儿子之外的所有子节点都是轻儿子。
2.3 重边(Heavy Edge)
- 父节点与其重儿子之间的边。
2.4 轻边(Light Edge)
- 父节点与其轻儿子之间的边。
2.5 重链(Heavy Path)
- 由重边连接形成的极大路径。
- 每条重链的起点是轻儿子或根节点,终点是叶子节点或重儿子的轻儿子。
✅ 关键性质:从任意节点到根的路径上,最多经过 O(log n) 条轻边,因此最多经过 O(log n) 条重链。
三、树链剖分的两个 DFS
3.1 第一次 DFS:计算子树大小、深度、父节点、重儿子
void dfs1(int u, int parent, int depth) {
dep[u] = depth;
fa[u] = parent;
sz[u] = 1; // 子树大小
hson[u] = 0; // 重儿子初始化为 0
for (int v : tree[u]) {
if (v == parent) continue;
dfs1(v, u, depth + 1);
sz[u] += sz[v];
if (sz[v] > sz[hson[u]]) {
hson[u] = v; // 更新重儿子
}
}
}
3.2 第二次 DFS:生成 DFS 序、分配链顶、构建线性序列
int dfn = 0; // DFS 序计数器
int top[MAXN]; // top[u] 表示 u 所在重链的顶部节点
int id[MAXN]; // id[u] 表示 u 在线段树中的位置(新编号)
void dfs2(int u, int tp) {
id[u] = ++dfn; // 分配新编号
top[u] = tp; // 记录链顶
if (hson[u]) {
dfs2(hson[u], tp); // 重儿子继承当前链顶
}
for (int v : tree[u]) {
if (v == fa[u] || v == hson[u]) continue;
dfs2(v, v); // 轻儿子开启新链,链顶是自己
}
}
🌟 重点:重儿子被优先访问,保证了同一条重链上的节点在线段树中是连续的。
四、树链剖分的应用:路径操作
4.1 路径查询/修改的核心思想
将 u 到 v 的路径拆成若干段重链,每段在 DFS 序上是连续的,可以用线段树区间操作。
4.2 核心函数:跳链过程
// 查询 u 到 v 路径上的最大值
int query_path(int u, int v) {
int res = -INF;
while (top[u] != top[v]) {
if (dep[top[u]] < dep[top[v]]) swap(u, v);
// u 的链顶更深,先处理 u 所在链
res = max(res, seg_tree.query(id[top[u]], id[u]));
u = fa[top[u]]; // 跳到链顶的父节点(轻边)
}
// 现在 u 和 v 在同一条链上
if (id[u] > id[v]) swap(u, v);
res = max(res, seg_tree.query(id[u], id[v]));
return res;
}
4.3 子树操作
子树操作更简单!因为子树的 DFS 序是连续的。
// 查询 u 的子树和
int query_subtree(int u) {
return seg_tree.query(id[u], id[u] + sz[u] - 1);
}
五、C++ 实现(支持路径加、路径最大值查询)
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 5;
const int INF = 0x3f3f3f3f;
// ======== 树结构 ========
int n, m, r;
vector<int> tree[MAXN];
int w[MAXN]; // 初始点权
// ======== 第一次 DFS 数据 ========
int dep[MAXN], fa[MAXN], sz[MAXN], hson[MAXN];
// ======== 第二次 DFS 数据 ========
int dfn = 0;
int id[MAXN], top[MAXN];
// ======== 线段树 ========
struct Node {
int l, r;
int sum, max_val;
int lazy;
} seg_tree[4 * MAXN];
void push_up(int u) {
seg_tree[u].sum = seg_tree[u*2].sum + seg_tree[u*2+1].sum;
seg_tree[u].max_val = max(seg_tree[u*2].max_val, seg_tree[u*2+1].max_val);
}
void push_down(int u) {
if (seg_tree[u].lazy) {
int lz = seg_tree[u].lazy;
seg_tree[u*2].sum += lz * (seg_tree[u*2].r - seg_tree[u*2].l + 1);
seg_tree[u*2+1].sum += lz * (seg_tree[u*2+1].r - seg_tree[u*2+1].l + 1);
seg_tree[u*2].max_val += lz;
seg_tree[u*2+1].max_val += lz;
seg_tree[u*2].lazy += lz;
seg_tree[u*2+1].lazy += lz;
seg_tree[u].lazy = 0;
}
}
void build(int u, int l, int r) {
seg_tree[u] = {l, r, 0, -INF, 0};
if (l == r) {
seg_tree[u].sum = seg_tree[u].max_val = w[r]; // w 是原点权
return;
}
int mid = (l + r) >> 1;
build(u*2, l, mid);
build(u*2+1, mid+1, r);
push_up(u);
}
void modify(int u, int l, int r, int val) {
if (seg_tree[u].l >= l && seg_tree[u].r <= r) {
seg_tree[u].sum += val * (seg_tree[u].r - seg_tree[u].l + 1);
seg_tree[u].max_val += val;
seg_tree[u].lazy += val;
return;
}
push_down(u);
int mid = (seg_tree[u].l + seg_tree[u].r) >> 1;
if (l <= mid) modify(u*2, l, r, val);
if (r > mid) modify(u*2+1, l, r, val);
push_up(u);
}
int query_sum(int u, int l, int r) {
if (seg_tree[u].l >= l && seg_tree[u].r <= r) {
return seg_tree[u].sum;
}
push_down(u);
int mid = (seg_tree[u].l + seg_tree[u].r) >> 1;
int res = 0;
if (l <= mid) res += query_sum(u*2, l, r);
if (r > mid) res += query_sum(u*2+1, l, r);
return res;
}
int query_max(int u, int l, int r) {
if (seg_tree[u].l >= l && seg_tree[u].r <= r) {
return seg_tree[u].max_val;
}
push_down(u);
int mid = (seg_tree[u].l + seg_tree[u].r) >> 1;
int res = -INF;
if (l <= mid) res = max(res, query_max(u*2, l, r));
if (r > mid) res = max(res, query_max(u*2+1, l, r));
return res;
}
// ======== 树链剖分 DFS ========
void dfs1(int u, int parent, int depth) {
dep[u] = depth;
fa[u] = parent;
sz[u] = 1;
hson[u] = 0;
for (int v : tree[u]) {
if (v == parent) continue;
dfs1(v, u, depth + 1);
sz[u] += sz[v];
if (sz[v] > sz[hson[u]]) {
hson[u] = v;
}
}
}
void dfs2(int u, int tp) {
id[u] = ++dfn;
top[u] = tp;
if (hson[u]) {
dfs2(hson[u], tp); // 重儿子继承链顶
}
for (int v : tree[u]) {
if (v == fa[u] || v == hson[u]) continue;
dfs2(v, v); // 轻儿子开启新链
}
}
// ======== 路径操作接口 ========
void modify_path(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 (id[u] > id[v]) swap(u, v);
modify(1, id[u], id[v], val);
}
int query_path_max(int u, int v) {
int res = -INF;
while (top[u] != top[v]) {
if (dep[top[u]] < dep[top[v]]) swap(u, v);
res = max(res, query_max(1, id[top[u]], id[u]));
u = fa[top[u]];
}
if (id[u] > id[v]) swap(u, v);
res = max(res, query_max(1, id[u], id[v]));
return res;
}
// ======== 子树操作接口 ========
void modify_subtree(int u, int val) {
modify(1, id[u], id[u] + sz[u] - 1, val);
}
int query_subtree_sum(int u) {
return query_sum(1, id[u], id[u] + sz[u] - 1);
}
// ======== 主函数 ========
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> n >> m >> r;
for (int i = 1; i <= n; ++i) {
cin >> w[i];
}
for (int i = 1; i <= n - 1; ++i) {
int u, v;
cin >> u >> v;
tree[u].push_back(v);
tree[v].push_back(u);
}
// 树链剖分预处理
dfs1(r, 0, 1);
dfs2(r, r);
build(1, 1, n);
// 处理 m 个操作
while (m--) {
int op, x, y, z;
cin >> op;
if (op == 1) { // 路径加
cin >> x >> y >> z;
modify_path(x, y, z);
} else if (op == 2) { // 路径最大值
cin >> x >> y;
cout << query_path_max(x, y) << '\n';
} else if (op == 3) { // 子树加
cin >> x >> z;
modify_subtree(x, z);
} else if (op == 4) { // 子树和
cin >> x;
cout << query_subtree_sum(x) << '\n';
}
}
return 0;
}
六、复杂度分析
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 预处理(两次 DFS) | O(n) | |
| 路径修改/查询 | O(log² n) | 每条链 O(log n),最多 O(log n) 条链 |
| 子树修改/查询 | O(log n) | 连续区间,一次线段树操作 |
✅ 路径操作 O(log² n) 是树链剖分的标准复杂度。
七、核心性质与优势
- 路径拆分高效:任意路径最多被拆分为 O(log n) 段重链。
- 支持多种操作:区间加、区间最值、区间和、子树操作等。
- 可扩展性强:可结合懒标记、多种线段树形态。
- 代码模式固定:两次 DFS + 跳链循环,易于模板化。
八、注意事项
hson[u]初始化为0,表示无重儿子。top[u]在dfs2中赋值。- 线段树要支持区间修改(懒标记)。
- 输入输出较大时使用
ios::sync_with_stdio(false)。
九、总结
树链剖分是将树的路径问题转化为线性区间问题的强大工具。其核心在于:
- 重轻儿子划分 → 形成重链
- 两次 DFS → 构建 DFS 序与链顶
- 跳链循环 → 将路径拆为 O(log n) 段
- 线段树维护 → 高效区间操作
574

被折叠的 条评论
为什么被折叠?



