树链剖分
代码模板树链剖分先让重儿子连续编号,把树上路径拆成 $O(\log n)$ 段数组区间,再交给线段树处理。
一句话算法
树链剖分先让重儿子连续编号,把树上路径拆成
问题模型
给定一棵有根树,每个点有权值,要求支持:
- 树上两点路径加;
- 树上两点路径求和;
- 某个子树整体加;
- 某个子树求和。
树上路径不是数组区间,不能直接套线段树。树链剖分的目标是把树上的路径拆成少量连续区间。
核心直觉
树剖只做一件核心的事:
DFS 编号时,永远优先走重儿子。
重儿子是子树最大的儿子。这样一来,一条不断走重儿子的链,会在 DFS 序里变成连续区间。
树上重链: 1 -> 3 -> 6 -> 9
DFS 编号:dfn[1], dfn[1]+1, dfn[1]+2, dfn[1]+3
路径查询时,如果两个点不在同一条重链,就把更深的链头到当前点这一段处理掉,然后跳到链头的父亲。每跳一次都跨过一条轻边,而轻边最多跨
关键数组
parent[u]:父亲;depth[u]:深度;subtree_size[u]:子树大小;heavy_son[u]:重儿子;top[u]:当前重链的链头;dfn[u]:DFS 新编号;node_at[dfn]:新编号对应的原节点。
子树也会变成连续区间:
算法步骤
第一次 DFS
- 计算
parent[u]、depth[u]。 - 统计
subtree_size[u]。 - 找到子树最大的儿子,记为
heavy_son[u]。
第二次 DFS
- 从链头开始给节点分配
dfn。 - 优先递归重儿子,让重链编号连续。
- 再递归所有轻儿子,每个轻儿子开启一条新链。
路径操作
处理路径 (u, v):
- 当
top[u] != top[v]:- 选择链头更深的一边;
- 在线段树上处理
[dfn[top[u]], dfn[u]]; - 令
u = parent[top[u]]。
- 最后两点在同一条重链上,处理
[dfn[u], dfn[v]]。
算法证明
核心不变量:每条重链在 DFS 序上是连续区间。
- 第二次 DFS 总是先访问重儿子,所以从链头沿重儿子一直走的节点会被连续编号。
- 轻儿子会开启新链,离开当前重链后才递归,因此不会插入到当前重链的编号中间。
- 子树是普通 DFS 序的连续区间,优先重儿子不会破坏 DFS 序的子树连续性。
为什么路径只会拆成
若从节点经过轻边跳到父亲所在链,说明刚离开的子树不是父亲的最大子树,所以它的大小至多是父亲子树的一半。
沿着轻边向上跳时,子树规模至少翻倍:
因此轻边数量最多是
复杂度分析
预处理两次 DFS:
- 时间复杂度:
。 - 空间复杂度:
。
每次路径修改或路径查询:
- 拆成
段; - 每段在线段树上操作
; - 总时间复杂度:
。
子树修改和子树查询是一次线段树区间操作,时间复杂度
代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
#include <bits/stdc++.h>
using namespace std;
struct SegmentTree {
vector<long long> sum;
vector<long long> lazy;
int mod = 1;
SegmentTree(int n = 0, int mod_value = 1) {
init(n, mod_value);
}
void init(int n, int mod_value) {
mod = mod_value;
sum.assign(n * 4 + 5, 0);
lazy.assign(n * 4 + 5, 0);
}
void apply(int u, int l, int r, long long value) {
value %= mod;
sum[u] = (sum[u] + value * (r - l + 1)) % mod;
lazy[u] = (lazy[u] + value) % mod;
}
void pushdown(int u, int l, int r) {
if (lazy[u] == 0 || l == r) return;
int mid = (l + r) >> 1;
apply(u << 1, l, mid, lazy[u]);
apply(u << 1 | 1, mid + 1, r, lazy[u]);
lazy[u] = 0;
}
void build(int u, int l, int r, const vector<long long> &base) {
if (l == r) {
sum[u] = base[l] % mod;
return;
}
int mid = (l + r) >> 1;
build(u << 1, l, mid, base);
build(u << 1 | 1, mid + 1, r, base);
sum[u] = (sum[u << 1] + sum[u << 1 | 1]) % mod;
}
void range_add(int ql, int qr, long long value, int u, int l, int r) {
if (ql <= l && r <= qr) {
apply(u, l, r, value);
return;
}
pushdown(u, l, r);
int mid = (l + r) >> 1;
if (ql <= mid) range_add(ql, qr, value, u << 1, l, mid);
if (qr > mid) range_add(ql, qr, value, u << 1 | 1, mid + 1, r);
sum[u] = (sum[u << 1] + sum[u << 1 | 1]) % mod;
}
long long range_sum(int ql, int qr, int u, int l, int r) {
if (ql <= l && r <= qr) return sum[u];
pushdown(u, l, r);
int mid = (l + r) >> 1;
long long answer = 0;
if (ql <= mid) answer += range_sum(ql, qr, u << 1, l, mid);
if (qr > mid) answer += range_sum(ql, qr, u << 1 | 1, mid + 1, r);
return answer % mod;
}
};
struct HeavyLightDecomposition {
int n;
int root;
int mod;
int timer = 0;
vector<vector<int>> graph;
vector<int> parent, depth, subtree_size, heavy_son;
vector<int> top, dfn, node_at;
vector<long long> value, ordered_value;
SegmentTree seg;
HeavyLightDecomposition(int n, int root, int mod)
: n(n), root(root), mod(mod),
graph(n + 1),
parent(n + 1), depth(n + 1), subtree_size(n + 1),
heavy_son(n + 1, 0), top(n + 1), dfn(n + 1), node_at(n + 1),
value(n + 1), ordered_value(n + 1),
seg(n, mod) {}
void add_edge(int u, int v) {
graph[u].push_back(v);
graph[v].push_back(u);
}
void dfs_size(int u, int father) {
parent[u] = father;
depth[u] = depth[father] + 1;
subtree_size[u] = 1;
heavy_son[u] = 0;
for (int v : graph[u]) {
if (v == father) continue;
dfs_size(v, u);
subtree_size[u] += subtree_size[v];
if (heavy_son[u] == 0 ||
subtree_size[v] > subtree_size[heavy_son[u]]) {
heavy_son[u] = v;
}
}
}
void dfs_decompose(int u, int chain_top) {
top[u] = chain_top;
dfn[u] = ++timer;
node_at[timer] = u;
ordered_value[timer] = value[u];
if (heavy_son[u] != 0) {
dfs_decompose(heavy_son[u], chain_top);
}
for (int v : graph[u]) {
if (v == parent[u] || v == heavy_son[u]) continue;
dfs_decompose(v, v);
}
}
void build() {
dfs_size(root, 0);
dfs_decompose(root, root);
seg.build(1, 1, n, ordered_value);
}
void path_add(int u, int v, long long delta) {
while (top[u] != top[v]) {
if (depth[top[u]] < depth[top[v]]) swap(u, v);
seg.range_add(dfn[top[u]], dfn[u], delta, 1, 1, n);
u = parent[top[u]];
}
if (depth[u] > depth[v]) swap(u, v);
seg.range_add(dfn[u], dfn[v], delta, 1, 1, n);
}
long long path_sum(int u, int v) {
long long answer = 0;
while (top[u] != top[v]) {
if (depth[top[u]] < depth[top[v]]) swap(u, v);
answer += seg.range_sum(dfn[top[u]], dfn[u], 1, 1, n);
answer %= mod;
u = parent[top[u]];
}
if (depth[u] > depth[v]) swap(u, v);
answer += seg.range_sum(dfn[u], dfn[v], 1, 1, n);
return answer % mod;
}
void subtree_add(int u, long long delta) {
seg.range_add(dfn[u], dfn[u] + subtree_size[u] - 1, delta, 1, 1, n);
}
long long subtree_sum(int u) {
return seg.range_sum(dfn[u], dfn[u] + subtree_size[u] - 1, 1, 1, n);
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, root, mod;
cin >> n >> m >> root >> mod;
HeavyLightDecomposition hld(n, root, mod);
for (int i = 1; i <= n; i++) {
cin >> hld.value[i];
hld.value[i] %= mod;
}
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
hld.add_edge(u, v);
}
hld.build();
while (m--) {
int op;
cin >> op;
if (op == 1) {
int x, y;
long long z;
cin >> x >> y >> z;
hld.path_add(x, y, z);
} else if (op == 2) {
int x, y;
cin >> x >> y;
cout << hld.path_sum(x, y) << '\n';
} else if (op == 3) {
int x;
long long z;
cin >> x >> z;
hld.subtree_add(x, z);
} else {
int x;
cin >> x;
cout << hld.subtree_sum(x) << '\n';
}
}
return 0;
}
负数取模语义不同:C++ 结果可为负,Python 恒非负;模板约定传入非负增量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# 树链剖分(HLD)+ 带懒标记的区间加、区间和线段树,全部对 mod 取模。
# 节点编号 1..n,dfn 是树剖序(1 下标):重链在 dfn 上连续,子树也连续,于是路径/子树都变成区间操作。
# 调用契约:hld = HeavyLightDecomposition(n, root, mod);填 hld.value[1..n](建议先 % mod);
# add_edge 建树;hld.build();之后用 path_add / path_sum / subtree_add / subtree_sum。
# SegmentTree 内部 sum/lazy 按 n * 4 + 5 开;mod 必须为正整数。
# C++ 的 % 对负数会给负结果,Python 的 % 恒非负;模意义下同余,最终答案一致。
# dfs_size / dfs_decompose 递归深度最坏 n(1e5+),使用前需 sys.setrecursionlimit(1 << 20)。
type Seq = list[int]
class SegmentTree:
"""区间加、区间和,sum[u] / lazy[u] 分别存节点 u 的和与待下传的加量。"""
sum: Seq
lazy: Seq
mod: int
def __init__(self, n: int = 0, mod_value: int = 1) -> None:
self.init(n, mod_value)
def init(self, n: int, mod_value: int) -> None:
self.mod = mod_value
self.sum = [0] * (n * 4 + 5)
self.lazy = [0] * (n * 4 + 5)
def apply(self, u: int, l: int, r: int, value: int) -> None:
value %= self.mod
# 区间长度是 r - l + 1;先取模再加,防止数值无限增长。
self.sum[u] = (self.sum[u] + value * (r - l + 1)) % self.mod
self.lazy[u] = (self.lazy[u] + value) % self.mod
def pushdown(self, u: int, l: int, r: int) -> None:
# 叶节点没有儿子可下传;lazy 为 0 时无事可做。
if self.lazy[u] == 0 or l == r:
return
mid = (l + r) >> 1 # 除以 2 向下取整
self.apply(u << 1, l, mid, self.lazy[u]) # 左儿子编号 2u
self.apply(u << 1 | 1, mid + 1, r, self.lazy[u]) # 右儿子编号 2u+1
self.lazy[u] = 0
def build(self, u: int, l: int, r: int, base: Seq) -> None:
if l == r:
self.sum[u] = base[l] % self.mod
return
mid = (l + r) >> 1
self.build(u << 1, l, mid, base)
self.build(u << 1 | 1, mid + 1, r, base)
self.sum[u] = (self.sum[u << 1] + self.sum[u << 1 | 1]) % self.mod
def range_add(self, ql: int, qr: int, value: int, u: int, l: int, r: int) -> None:
if ql <= l and r <= qr:
self.apply(u, l, r, value)
return
self.pushdown(u, l, r)
mid = (l + r) >> 1
if ql <= mid:
self.range_add(ql, qr, value, u << 1, l, mid)
if qr > mid:
self.range_add(ql, qr, value, u << 1 | 1, mid + 1, r)
self.sum[u] = (self.sum[u << 1] + self.sum[u << 1 | 1]) % self.mod
def range_sum(self, ql: int, qr: int, u: int, l: int, r: int) -> int:
if ql <= l and r <= qr:
return self.sum[u]
self.pushdown(u, l, r)
mid = (l + r) >> 1
answer = 0
if ql <= mid:
answer += self.range_sum(ql, qr, u << 1, l, mid)
if qr > mid:
answer += self.range_sum(ql, qr, u << 1 | 1, mid + 1, r)
return answer % self.mod
class HeavyLightDecomposition:
"""两遍 DFS 定重儿子和 dfn,再用线段树维护路径/子树操作。"""
n: int
root: int
mod: int
timer: int
graph: list[list[int]]
parent: Seq
depth: Seq
subtree_size: Seq
heavy_son: Seq
top: Seq
dfn: Seq
node_at: Seq
value: Seq
ordered_value: Seq
seg: SegmentTree
def __init__(self, n: int, root: int, mod: int) -> None:
self.n = n
self.root = root
self.mod = mod
self.timer = 0
self.graph = [[] for _ in range(n + 1)]
self.parent = [0] * (n + 1)
self.depth = [0] * (n + 1)
self.subtree_size = [0] * (n + 1)
# heavy_son 用 0 表示没有重儿子,subtree_size[0] = 0 让比较天然成立。
self.heavy_son = [0] * (n + 1)
self.top = [0] * (n + 1)
self.dfn = [0] * (n + 1)
self.node_at = [0] * (n + 1)
self.value = [0] * (n + 1)
self.ordered_value = [0] * (n + 1)
self.seg = SegmentTree(n, mod)
def add_edge(self, u: int, v: int) -> None:
self.graph[u].append(v)
self.graph[v].append(u)
def dfs_size(self, u: int, father: int) -> None:
self.parent[u] = father
self.depth[u] = self.depth[father] + 1
self.subtree_size[u] = 1
self.heavy_son[u] = 0
for v in self.graph[u]:
if v == father:
continue
self.dfs_size(v, u)
self.subtree_size[u] += self.subtree_size[v]
if self.heavy_son[u] == 0 or self.subtree_size[v] > self.subtree_size[self.heavy_son[u]]:
self.heavy_son[u] = v
def dfs_decompose(self, u: int, chain_top: int) -> None:
self.top[u] = chain_top
self.timer += 1
self.dfn[u] = self.timer
self.node_at[self.timer] = u
self.ordered_value[self.timer] = self.value[u]
# 先走重儿子,保证重链在 dfn 上连续。
if self.heavy_son[u] != 0:
self.dfs_decompose(self.heavy_son[u], chain_top)
for v in self.graph[u]:
if v == self.parent[u] or v == self.heavy_son[u]:
continue
self.dfs_decompose(v, v)
def build(self) -> None:
self.dfs_size(self.root, 0)
self.dfs_decompose(self.root, self.root)
self.seg.build(1, 1, self.n, self.ordered_value)
def path_add(self, u: int, v: int, delta: int) -> None:
while self.top[u] != self.top[v]:
# 链顶浅的那条整段往上跳,跳完接上链顶的父亲。
if self.depth[self.top[u]] < self.depth[self.top[v]]:
u, v = v, u
self.seg.range_add(self.dfn[self.top[u]], self.dfn[u], delta, 1, 1, self.n)
u = self.parent[self.top[u]]
# 同一条链上:浅的是 LCA,区间就是 [dfn[u], dfn[v]]。
if self.depth[u] > self.depth[v]:
u, v = v, u
self.seg.range_add(self.dfn[u], self.dfn[v], delta, 1, 1, self.n)
def path_sum(self, u: int, v: int) -> int:
answer = 0
while self.top[u] != self.top[v]:
if self.depth[self.top[u]] < self.depth[self.top[v]]:
u, v = v, u
answer += self.seg.range_sum(self.dfn[self.top[u]], self.dfn[u], 1, 1, self.n)
answer %= self.mod
u = self.parent[self.top[u]]
if self.depth[u] > self.depth[v]:
u, v = v, u
answer += self.seg.range_sum(self.dfn[u], self.dfn[v], 1, 1, self.n)
return answer % self.mod
def subtree_add(self, u: int, delta: int) -> None:
self.seg.range_add(self.dfn[u], self.dfn[u] + self.subtree_size[u] - 1, delta, 1, 1, self.n)
def subtree_sum(self, u: int) -> int:
return self.seg.range_sum(self.dfn[u], self.dfn[u] + self.subtree_size[u] - 1, 1, 1, self.n)
测试用例
输入:
5 5 1 100
1 2 3 4 5
1 2
1 3
3 4
3 5
2 4 5
1 2 4 1
2 4 5
3 3 2
4 3
输出:
12
14
20
解释:
- 初始路径
4 -> 3 -> 5的和为4 + 3 + 5 = 12; - 路径
2 -> 4加1后,点1,2,3,4都增加; - 子树
3加2后,点3,4,5都增加,子树和变为20。
应用分类详解
树链剖分的本质是把树上路径转成少量数组区间。只要题目要求频繁操作树上路径,就应该考虑树剖。
一、路径修改和路径查询
典型模式: 两点之间路径加、路径求和、路径最大值。
识别信号: 出现“
核心建模: 用树剖拆路径,用线段树维护区间信息。
| 应用场景 | 经典题目 | 核心思路 |
|---|---|---|
| 树剖模板 | luogu-P3384 | 路径和 + 子树和 |
| 树上路径最大值 | luogu-P2590 | 线段树维护最大值 |
二、子树整体操作
典型模式: 修改某个点的整棵子树,或查询子树权值和。
识别信号: 出现“以
核心建模: 子树在 DFS 序中是连续区间 [dfn[u], dfn[u]+size[u]-1]。
| 应用场景 | 经典题目 | 核心思路 |
|---|---|---|
| 子树加与子树求和 | luogu-P3384 | DFS 序区间操作 |
| 树上统计 | 子树贡献类题目 | 把子树压成数组区间 |
三、路径拆分后的复合维护
典型模式: 线段树节点维护的不只是和,还可能是最大值、最小值、颜色段、矩阵等。
识别信号: 路径信息能通过区间合并得到。
核心建模: 树剖只负责拆路径;真正维护什么信息由线段树决定。
经典例题
1. luogu-P3384
树链剖分模板题,完整覆盖路径加、路径和、子树加、子树和。
2. luogu-P2590
树上路径最大值。把线段树维护内容从区间和换成区间最大值即可。
3. luogu-P4114
路径最大边权类问题。适合练习边权转点权,以及路径查询时如何排除 LCA 点。
参考
- 本目录草稿:
book/pages/tree-algo/hld/灵魂.md - 本书相关章节:DFS 序