把「点到根路径和」倒过来算成每次修改对询问的贡献:单点加变成子树区间加常数,子树加拆成 a·dep[x] 与常数两项,DFS 序上两棵「区间加、单点查」树状数组即可。核心是点对子树的贡献,DFS 序差分数组的前缀和求出单点被贡献的值。
启发记录: 把「根路径和」倒过来算成修改对询问的贡献,对偶视角的好入口
OJ: luogu
题目 ID: P3178
难度:提高+/省选-
标签:DFS序树状数组差分贡献法树链剖分括号序python
创建: 2026-07-17 02:00
更新: 2026-10-27 09:35
目录
形式化题目
有一棵以
1 x a:; 2 x a:对子树内每个节点 令 ; 3 x:输出,其中 是 到根的路径上的点集。
每次询问按顺序输出一行答案。
解法总览
三个操作分别作用在「单点」「子树」「根路径」上,而询问只有一种:根路径和。下面四种做法的区别在于选择哪一侧做等价变换:可以像树链剖分那样把路径拆成区间(维护点权),也可以反过来把「路径和」看成「修改对询问的贡献」(维护答案)。
| 解法 | 核心观察 | 复杂度 | 适用场景 | 对应代码 |
|---|---|---|---|---|
| 暴力解法 | 直接按题意模拟,子树加逐点递归、路径和逐点上跳 | 只适合 |
brute.cpp |
|
| 解法一(正式主解) | 每条修改只影响一棵子树内的询问;子树加的影响按 |
求和类询问;只需「子树是 DFS 序区间」这一个性质 | main.cpp、main.py |
|
| 解法二 | 括号序上「入栈加、出栈减」,前缀和天然等于根路径和 | 同上,但需要长度为 |
main2.cpp |
|
| 解法三 | 树链剖分把子树与根路径都变成 |
询问换成 max/min 等不可差分的量时仍可用 | main3.cpp |
正式主解取解法一(main.cpp,Python 版 main.py)。三条正确解法里它与解法二同阶,取舍点在「少一层概念」而不是「少几行代码」:解法一唯一依赖的性质是「子树是 DFS 序上的连续区间」,直接由题面的「子树加」读出来;解法二额外引入「括号序前缀和 = 根路径和」这层带符号的对应关系,代码反而少几行(见「复杂度对比」的行数一列),但数组要开到
暴力解法
思路
brute.cpp 忠实按题意模拟:数组 w[] 存当前点权,操作 1 直接改一个点;操作 2 从 parent 一路走到根累加。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-08-12 23:00
* update_at: 2026-08-12 23:00
*/
// brute.cpp:小数据暴力解,直接模拟三种操作,用来理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int n, m;
long long w[MAXN]; // w[i] 表示节点 i 当前的权值
int parent[MAXN]; // 预处理出的父亲
vector<int> g[MAXN]; // 树的邻接表
// 以 u 为根的子树整体加 value(u 是根节点 1,fa 用来防止回头走)。
void subtree_add(int u, int fa, long long value) {
w[u] += value;
for (int j = 0; j < (int)g[u].size(); j++) {
int v = g[u][j];
if (v != fa) subtree_add(v, u, value);
}
}
// 求从 x 走到根 1 的路径点权和:不断沿 parent 上跳累加。
long long root_path_sum(int x) {
long long answer = 0;
while (x != 0) {
answer += w[x];
x = parent[x];
}
return answer;
}
// 预处理 parent:从根 1 出发遍历整棵树。
void build_parent(int u, int fa) {
parent[u] = fa;
for (int j = 0; j < (int)g[u].size(); j++) {
int v = g[u][j];
if (v != fa) build_parent(v, u);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> w[i];
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
build_parent(1, 0);
while (m--) {
int opt, x;
long long a;
cin >> opt >> x;
if (opt == 1) { // 单点加
cin >> a;
w[x] += a;
} else if (opt == 2) { // 子树整体加:递归访问子树所有点
cin >> a;
subtree_add(x, parent[x], a);
} else { // 询问根路径和:沿 parent 一路加到根
cout << root_path_sum(x) << '\n';
}
}
return 0;
}复杂度与瓶颈
- 时间:操作 1 是
,操作 2 是 ,操作 3 是 ,最坏都到 ,总计 ;链状树上 次叶子询问就让总代价达到 量级。 - 空间:
。
瓶颈很明确:每次操作都要把一整棵子树或整条路径走一遍。要让单次操作降到对数级,必须让「子树」和「根路径」都不用逐个节点访问,只留下少数几段连续区间去处理。
解法一:DFS 序 + 贡献法(正式主解)
思路
先做一件不改变答案的事:把视角从「查询时去树上数路径」换成「修改时就结算它对未来询问的贡献」。查询点在 DFS 序上是个单点,而修改的作用范围是整棵子树——一次 DFS 就够,不需要剖分。
准备工作:一次 DFS 求出每个节点的进入时间戳 dfn_in[x]、子树右端点 dfn_out[x] 与深度 dep[x]。子树
操作 1 的贡献:dfn_in[x] 处的累计值。
操作 2 的贡献:子树
第一项只和查询点深度有关,第二项对子树内所有
操作 3 的答案:先把两棵树的含义定死,后面的公式才不会糊。
| 树 | 代码里的名字 | 每个位置记什么 | 查询时怎么用 | 谁往里写 |
|---|---|---|---|---|
| 常数树 | bit_const |
每次修改里与深度无关的那部分 | 直接取前缀和 | 操作 1、操作 2 的常数项、初始权值 |
| 系数树 | bit_depth |
每次修改里要乘深度的那部分系数 | 取前缀和后再乘 |
操作 2 |
记 dfn_in[x] 上取值,所以答案是
公式只是把上表两棵树的结果拼起来:凡是加在
把操作 2 代进去验一遍:子树
正是「bit_const 与 bit_depth 两个树状数组:一个只存常数,一个只存深度系数。
需要的工具恰好是最基础的那一套:区间加、单点查。把差分数组
以题面样例的树(边
| 节点 |
1 | 2 | 3 | 5 | 4 |
|---|---|---|---|---|---|
dfn_in[x] |
1 | 2 | 3 | 4 | 5 |
| 子树区间 | [1,5] | [2,4] | [3,3] | [4,4] | [5,5] |
| 1 | 2 | 3 | 3 | 2 |
表格里节点的排列顺序就是 DFS 序:节点 2 的子树
下面直接在树上把这六个步骤画出来:每个节点圈里的下行两个数,就是它此刻攒到的「常数 / 系数」。DFS 序只是把树映射成数组的手段,两棵树真正作用的对象是节点,所以演示不必绕到数组上去看。样例一共三次查询、两次修改,每次修改只动一棵树的若干节点,查询时把绿圈那个节点的两个数按 常数 + 深度 × 系数 合成答案。
上面是静态版的六步。想自己一步步走,可以打开 交互演示:点一步看一步,先猜后揭示:每步先让你猜「这次哪些节点的常数会变」或「答案是多少」,选完再揭示,支持键盘左右键切换与自动播放。页面自带四个预设(题面样例、链、星形、深浅两支),也可以把任意一组小数据按题面输入格式粘进去,数值全部在浏览器里实时算出。
同一过程的紧凑表格如下(常数树、系数树的前缀都取在 dfn_in[x] 这一处):
| 操作 | 转成的区间操作 | 常数树前缀 / 系数树前缀(在 dfn_in[x] 处) |
输出 |
|---|---|---|---|
| 初始权值 | 每个点 |
每个点 |
— |
3 3 |
— | ||
1 2 1 |
子树 |
— | — |
3 5 |
— | ||
2 1 2 |
子树 |
— | — |
3 3 |
— |
三次输出
代码
C++ 版(正式主解):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-23 20:05
* update_at: 2026-10-27 09:35
*/
// main.cpp:P3178 树上操作,DFS 序 + 贡献法 + 两棵差分树状数组。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 100005;
int n, m;
ll weight[MAXN]; // 每个点的初始权值
vector<int> g[MAXN]; // 树的邻接表
int dfn_in[MAXN]; // 节点的进入时间戳
int dfn_out[MAXN]; // 节点子树在时间戳上的右端点,子树区间为 [dfn_in[x], dfn_out[x]]
int depth[MAXN]; // 深度,根为 1
int timer; // 时间戳计数器
// 「区间加、单点查」树状数组:区间 [left, right] 加 value 只需在差分数组上改两处,
// 单点查 pos 的值就是差分数组的前缀和。
struct Fenwick {
ll tree[MAXN];
void add(int pos, ll value) {
for (int i = pos; i <= n; i += i & -i) tree[i] += value;
}
ll prefix_sum(int pos) {
ll answer = 0;
for (int i = pos; i > 0; i -= i & -i) answer += tree[i];
return answer;
}
void range_add(int left, int right, ll value) {
add(left, value);
add(right + 1, -value); // right + 1 超过 n 时,上面的循环自然不执行
}
};
Fenwick bit_const; // 贡献中与查询点深度无关的部分
Fenwick bit_depth; // 贡献中 depth[查询点] 的系数
// 一次 DFS 求出进入时间戳、子树右端点与深度。
void dfs(int u, int fa) {
dfn_in[u] = ++timer;
depth[u] = depth[fa] + 1;
for (int v : g[u]) {
if (v != fa) dfs(v, u);
}
dfn_out[u] = timer; // 回溯时 timer 恰好停在子树的最后一个节点上
}
// 节点 x 加 value:x 及其每个后代作为查询点时,路径和都正好多 value。
void point_add(int x, ll value) {
bit_const.range_add(dfn_in[x], dfn_out[x], value);
}
// 子树 x 加 value:子树内的查询点 y 多出 value * (depth[y] - depth[x] + 1),
// 拆成 value * depth[y] 与 value * (1 - depth[x]) 两部分分别累计。
void subtree_add(int x, ll value) {
bit_depth.range_add(dfn_in[x], dfn_out[x], value);
bit_const.range_add(dfn_in[x], dfn_out[x], value * (1 - depth[x]));
}
// 查询 x 到根的路径点权和。
ll root_path_sum(int x) {
return bit_const.prefix_sum(dfn_in[x]) + depth[x] * bit_depth.prefix_sum(dfn_in[x]);
}
int main() {
// 输入量到 1e5 级别,关掉同步后 cin 与 scanf 同量级。
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> weight[i];
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1, 0);
// 初始权值等价于对每个点各做一次「操作 1」。
for (int i = 1; i <= n; i++) point_add(i, weight[i]);
while (m--) {
int opt, x;
ll a;
cin >> opt >> x;
if (opt == 1) {
cin >> a;
point_add(x, a);
} else if (opt == 2) {
cin >> a;
subtree_add(x, a);
} else {
cout << root_path_sum(x) << '\n';
}
}
return 0;
}Python 版(同一算法):
import sys
def main():
data = list(map(int, sys.stdin.buffer.read().split()))
ptr = 0
n, m = data[ptr], data[ptr + 1]
ptr += 2
weight = [0] * (n + 1)
for i in range(1, n + 1):
weight[i] = data[ptr]
ptr += 1
graph = [[] for _ in range(n + 1)]
for _ in range(n - 1):
u, v = data[ptr], data[ptr + 1]
ptr += 2
graph[u].append(v)
graph[v].append(u)
dfn_in = [0] * (n + 1)
dfn_out = [0] * (n + 1)
depth = [0] * (n + 1)
timer = 0
# 一次 DFS 求出进入时间戳、子树右端点与深度。
# 判题器栈空间有限,用 (节点, 父亲, 状态) 的显式栈复现递归顺序:
# 状态 0 表示刚进入节点,状态 1 表示孩子都处理完了、即将回溯。
stack = [(1, 0, 0)]
while stack:
node, parent, state = stack.pop()
if state == 0:
timer += 1
dfn_in[node] = timer
depth[node] = depth[parent] + 1
stack.append((node, parent, 1))
for neighbor in graph[node]:
if neighbor != parent:
stack.append((neighbor, node, 0))
else:
dfn_out[node] = timer # 回溯时 timer 正好停在子树最后一个节点
bit_const = [0] * (n + 2) # 贡献中与查询点深度无关的部分
bit_depth = [0] * (n + 2) # 贡献中 depth[查询点] 的系数
def add(tree, index, value):
while index <= n:
tree[index] += value
index += index & -index
def prefix_sum(tree, index):
total = 0
while index > 0:
total += tree[index]
index -= index & -index
return total
def range_add(tree, left, right, value):
add(tree, left, value)
add(tree, right + 1, -value) # right + 1 超过 n 时 add 自然不执行
# 初始权值等价于对每个点各做一次「操作 1」。
for node in range(1, n + 1):
range_add(bit_const, dfn_in[node], dfn_out[node], weight[node])
answers = []
for _ in range(m):
operation = data[ptr]
ptr += 1
x = data[ptr]
ptr += 1
if operation == 3:
# 与深度无关的增量 + depth[x] 乘上深度系数
answers.append(
str(prefix_sum(bit_const, dfn_in[x])
+ depth[x] * prefix_sum(bit_depth, dfn_in[x]))
)
continue
a = data[ptr]
ptr += 1
if operation == 1:
# 节点 x 加 a:x 子树内每个查询点的路径和都正好多 a。
range_add(bit_const, dfn_in[x], dfn_out[x], a)
else:
# 子树 x 加 a:子树内查询点 y 多出 a*(depth[y]-depth[x]+1),
# 拆成 a*depth[y] 与 a*(1-depth[x]) 两部分分别累计。
range_add(bit_depth, dfn_in[x], dfn_out[x], a)
range_add(bit_const, dfn_in[x], dfn_out[x], a * (1 - depth[x]))
sys.stdout.write("\n".join(answers) + ("\n" if answers else ""))
if __name__ == "__main__":
main()复杂度
- 预处理:一次 DFS 是
;把 个初始权值各做一次「操作 1」是 。 - 操作 1:一次区间加,即 2 次树状数组单点加,
。 - 操作 2:两次区间加,
。 - 操作 3:两次前缀和,
。 - 合计
,空间 。
解法二:括号序(出栈入栈序)
思路
解法一把「路径和」倒过来算成了子树上的区间加。还有另一条同样只用一次 DFS 的路:不借助贡献拆分的思想,而是直接把根路径和变成序列上的一个前缀和。
把 DFS 改成「进入节点时记一次、离开节点时再记一次」,就得到长度为
为什么成立:把前缀
是 的祖先(含 自身): ,两个位置只有一个落进前缀,于是 恰好贡献 。 在 之前就已经访问完(例如 的某个祖先的更早的儿子所在的子树): 与 都在前缀内, 与 配对抵消成 。 要到 之后才访问(排在 后面的兄弟子树,以及更外层的节点):两个位置都大于 ,根本不出现在前缀里,贡献为 。
三类加起来只剩「祖先链上的那些
以样例的树为例(边
括号序: (1 (2 (3 )3 (5 )5 )2 (4 )4 )1
位置: 1 2 3 4 5 6 7 8 9 10
带符号: +w1 +w2 +w3 -w3 +w5 -w5 -w2 +w4 -w4 -w1查节点 5 的根路径和,取前 5 位相加:
三种操作在括号序上都很轻。两棵树分别存「常数项」与「深度系数」,后者在查询时还要乘上
- 操作 1(点
加 ):只在常数项树上做一对括号点, 处 、 处 。前缀和的影响范围正好是 及其每个后代, 的兄弟子树不受影响。 - 操作 2(子树
加 ):子树内的查询点 多出 ,与解法一同样的两项拆分;区别是两项都写成括号序上的成对单点——系数树在 处放 ,常数树在 处放 ,不再需要区间加。 - 操作 3(查
):在 处取两棵树各一次前缀和。两棵树的含义与解法一完全相同—— bit_const存与深度无关的部分,bit_depth存要乘深度的系数:
其中
两棵「单点加、前缀和」的普通树状数组就够(长度
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-23 20:14
* update_at: 2026-10-26 22:42
*/
// main2.cpp:P3178 树上操作,括号序(出栈入栈序)+ 两棵 2n 树状数组。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 100005;
int n, m;
ll weight[MAXN]; // 每个点的初始权值
vector<int> g[MAXN]; // 树的邻接表
int pos_in[MAXN]; // 节点入栈时在括号序里的位置
int pos_out[MAXN]; // 节点出栈时在括号序里的位置
int depth[MAXN]; // 深度,根为 1
int timer; // 括号序计数器,取值 1..2n
int seq_len; // 括号序长度,等于 2n
// 括号序上的普通树状数组:单点加、前缀求和。
struct Fenwick {
ll tree[2 * MAXN]; // 每个点入栈、出栈各占一个位置
void add(int pos, ll value) {
for (int i = pos; i <= seq_len; i += i & -i) tree[i] += value;
}
ll prefix_sum(int pos) {
ll answer = 0;
for (int i = pos; i > 0; i -= i & -i) answer += tree[i];
return answer;
}
};
Fenwick bit_const; // 与查询点深度无关的那部分
Fenwick bit_depth; // depth[查询点] 的系数
// 一次 DFS 生成括号序:进入时分配一个位置,退出时再分配一个位置。
void dfs(int u, int fa) {
pos_in[u] = ++timer;
depth[u] = depth[fa] + 1;
for (int v : g[u]) {
if (v != fa) dfs(v, u);
}
pos_out[u] = ++timer;
}
// 在括号序的入栈、出栈两处做 ±value,使前缀和只统计到还留在栈里的节点。
void bracket_add(Fenwick &bit, int x, ll value) {
bit.add(pos_in[x], value);
bit.add(pos_out[x], -value);
}
int main() {
scanf("%d%d", &n, &m);
seq_len = 2 * n;
for (int i = 1; i <= n; i++) scanf("%lld", &weight[i]);
for (int i = 1; i < n; i++) {
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1, 0);
// 初始权值等价于对每个点各做一次「操作 1」。
for (int i = 1; i <= n; i++) bracket_add(bit_const, i, weight[i]);
while (m--) {
int opt, x;
ll a;
scanf("%d%d", &opt, &x);
if (opt == 1) {
// 节点 x 加 a:x 及其每个后代 y 的路径和都正好多 a。
scanf("%lld", &a);
bracket_add(bit_const, x, a);
} else if (opt == 2) {
// 子树 x 加 a:子树内的查询点 y 多出 a * (depth[y] - depth[x] + 1),
// 拆成 a * depth[y] 与 a * (1 - depth[x]) 两部分分别累计。
scanf("%lld", &a);
bracket_add(bit_depth, x, a);
bracket_add(bit_const, x, a * (1 - depth[x]));
} else {
// 入栈位置的前缀和只保留「入了栈还没出栈」的节点,它们恰好是 x 到根的路径。
ll answer = bit_const.prefix_sum(pos_in[x])
+ depth[x] * bit_depth.prefix_sum(pos_in[x]);
printf("%lld\n", answer);
}
}
return 0;
}复杂度
- 预处理:一次 DFS
; 次初始权值各 2 次单点加, 。 - 每次操作:常数次单点加或前缀和,
。 - 合计
,空间 (树状数组长度 ,常数比解法一略大)。
解法三:树链剖分 + 双树状数组
思路
这是本题最通行的模板解,方向与解法一相反:维护点权本身,把查询的路径拆成少量连续区间去求和。
- 树链剖分第二遍 DFS 优先走重儿子,于是每条重链在编号上连续。从
往根跳,每跨过一条轻边子树大小至少翻倍,所以根路径至多被拆成 条重链段,每段是一个连续区间 。 - 剖分序仍然是 DFS 序,子树
依然是连续区间 。 - 于是三种操作都变成「区间加」和「区间和」。区间加、区间和交给双树状数组:差分
上区间加只改两个端点,前缀和按
恢复,两个和分别由两棵树维护(rbook 模板 fenwick-range-add-sum)。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-08-12 23:00
* update_at: 2026-10-23 20:30
*/
// main3.cpp:P3178 树上操作,树链剖分 + 双树状数组(区间加、区间和)。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 100005;
int n, m;
ll weight[MAXN]; // 每个点的初始权值
vector<int> g[MAXN]; // 树的邻接表
// 树链剖分相关数组
int parent[MAXN]; // 父亲
int depth[MAXN]; // 深度,根为 1
int subsize[MAXN]; // 子树大小
int heavy[MAXN]; // 重儿子(子树最大的那个儿子)
int chain_top[MAXN]; // 重链链头
int dfn[MAXN]; // 剖分后的新编号 1..n
int timer; // dfn 分配计数器
// 双树状数组:同时支持区间加与区间和。
// bit_diff 存差分 b[i],bit_weighted 存 i * b[i],
// 前缀和按 P(x) = (x+1) * sum(b, x) - sum(i * b, x) 恢复。
struct RangeFenwick {
ll bit_diff[MAXN];
ll bit_weighted[MAXN];
void add(ll *bit, int pos, ll value) {
for (int i = pos; i <= n; i += i & -i) bit[i] += value;
}
ll sum(ll *bit, int pos) {
ll answer = 0;
for (int i = pos; i > 0; i -= i & -i) answer += bit[i];
return answer;
}
// 原数组区间 [left, right] 每个位置都加上 value
void range_add(int left, int right, ll value) {
add(bit_diff, left, value);
add(bit_diff, right + 1, -value);
add(bit_weighted, left, value * left);
add(bit_weighted, right + 1, -value * (right + 1));
}
ll prefix_sum(int pos) {
return (ll)(pos + 1) * sum(bit_diff, pos) - sum(bit_weighted, pos);
}
ll range_sum(int left, int right) {
return prefix_sum(right) - prefix_sum(left - 1);
}
};
RangeFenwick bit;
// 第一次 DFS:求父亲、深度、子树大小与重儿子。
void dfs_size(int u, int fa) {
parent[u] = fa;
depth[u] = depth[fa] + 1;
subsize[u] = 1;
for (int v : g[u]) {
if (v == fa) continue;
dfs_size(v, u);
subsize[u] += subsize[v];
if (subsize[v] > subsize[heavy[u]]) heavy[u] = v;
}
}
// 第二次 DFS:优先给重儿子编号,保证每条重链编号连续,子树编号也连续。
void dfs_number(int u, int top_node) {
chain_top[u] = top_node;
dfn[u] = ++timer;
if (heavy[u] != 0) dfs_number(heavy[u], top_node);
for (int v : g[u]) {
if (v != parent[u] && v != heavy[u]) dfs_number(v, v);
}
}
// 查询从 x 到根 1 的路径点权和:拆成若干条重链区间求和。
ll root_path_sum(int x) {
ll answer = 0;
while (chain_top[x] != chain_top[1]) {
answer += bit.range_sum(dfn[chain_top[x]], dfn[x]);
x = parent[chain_top[x]];
}
answer += bit.range_sum(dfn[1], dfn[x]);
return answer;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%lld", &weight[i]);
for (int i = 1; i < n; i++) {
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
dfs_size(1, 0);
dfs_number(1, 1);
// 初始权值:按剖分编号逐点放入(单点退化成区间加)。
for (int i = 1; i <= n; i++) bit.range_add(dfn[i], dfn[i], weight[i]);
while (m--) {
int opt, x;
ll a;
scanf("%d%d", &opt, &x);
if (opt == 1) { // 单点加:区间加退化成单点
scanf("%lld", &a);
bit.range_add(dfn[x], dfn[x], a);
} else if (opt == 2) { // 子树加:子树在剖分序上是连续区间
scanf("%lld", &a);
bit.range_add(dfn[x], dfn[x] + subsize[x] - 1, a);
} else { // 根路径和:逐段重链区间求和
printf("%lld\n", root_path_sum(x));
}
}
return 0;
}复杂度
- 预处理:两次 DFS
,初始权值入树 。 - 操作 1 / 2:一次区间加,
。 - 操作 3:
段重链 × 每段 ,总 。 - 合计
,空间 。
复杂度对比
| 解法 | 时间 | 空间 | 需要的结构 | 代码行数(不计注释与空行) |
|---|---|---|---|---|
| 暴力解法 | 邻接表 + 父亲数组 | 57 | ||
解法一 main.cpp |
1 次 DFS + 2 棵 |
75 | ||
解法二 main2.cpp |
1 次 DFS + 2 棵 |
68 | ||
解法三 main3.cpp |
2 次 DFS + 双树状数组 | 95 |
解法一与解法二是同阶的两条思路,区别只在把路径和变成「子树区间加 + 单点查」还是「括号序上的单点加 + 前缀和」;行数上解法二反而略短,但多一层「带符号括号序」的概念。解法三的复杂度多一个
实测(roj-local-judge-lite 限制 3000 ms,三条实现都 AC):
| 数据形状 | 解法一 main.cpp |
解法二 main2.cpp |
解法三 main3.cpp |
|---|---|---|---|
| 随机树 | 78 ms | 76 ms | 86 ms |
| 完全二叉树(根路径跨最多轻边) | 63 ms | 62 ms | 72 ms |
解法一与解法二每次操作调用树状数组的次数完全相同(操作 1 两次单点加、操作 2 四次、操作 3 两次前缀和),只差数组长度
解法三多出来的那个
总结
这道题的关键不是「树链剖分模板」,而是把「根路径和」这个查询倒过来看:
参考与致谢
本题的解法评估参考了洛谷公开题解,思路来源与本文自主完成的部分区分如下:
| 来源与作者 | 题解标题 / 链接 | 采纳与参考内容 | 本文独立完成内容 |
|---|---|---|---|
| zht467(uid 8220) | 题解 P3178 【[HAOI2015]树上操作】 | 「单点加等于给整棵子树加同一常数;子树加对查询点的增量是 |
把两项改写成「两棵差分树状数组的区间加 + 单点查」,重写现代 C++17 与 Python 代码;补充括号序解法与树链剖分解法作为并列对照;自行完成 545 组独立对拍与 |
| __wfx(uid 162451) | 题解 P3178 【[HAOI2015]树上操作】 | 「在出栈入栈序上,入栈加、出栈减,前缀和即根路径和」这一性质 | 独立给出前缀和等于根路径和的区间包含关系论证;改写为两棵「单点加、前缀和」树状数组(原文用 |
| benny(uid 44356) | 题解 P3178 【[HAOI2015]树上操作】 | 树链剖分 + 双树状数组(区间加、区间和)路线的完整模板形态 | 重新实现 main3.cpp:两遍 DFS 改为直接递归、RangeFenwick 去模板化并统一为 1 起始下标;补齐复杂度与栈深度实测 |
| yijan(uid 63398) | 题解 P3178 【[HAOI2015]树上操作】 | 交叉核对括号序路线与树链剖分路线的等价性(其题解同时给出两种实现) | 用于验证解法二与解法三在同一批随机数据上结论一致,未采用其代码结构 |
正文全部算式、复杂度分析与代码均为本仓库自行撰写;公开题解只提供上述关键观察。所有四种实现(brute.cpp、main.cpp、main2.cpp、main3.cpp 与 main.py)已在 545 组小数据上与独立暴力对拍一致,并在