贡献视角改写询问:DFS 序把子树变连续区间,单点加与子树加都化为区间加,子树加按 depth 拆成一次项与常数项两棵树状数组,点查即得根到 x 的路径和。
OJ: roj
题目 ID: 1561
难度:普及+/提高-
标签:DFS序树树状数组python
创建: 2026-09-30 18:11
更新: 2026-10-07 12:15
形式化题目
给定一棵以点
- 单点加:
; - 子树加:对以
为根的子树中每个点 , ; - 路径询问:求根到
的路径(即 的全部祖先,含自身)上所有点权之和
题面给出的输入样例缺了首行 N M(样例按 N M)读入。下图为样例的树(括号内为深度
graph TD
n1["1: d=0, w=1"] --> n2["2: d=1, w=2"]
n1 --> n4["4: d=1, w=4"]
n2 --> n3["3: d=2, w=3"]
n2 --> n5["5: d=2, w=5"]
图中每条有向边都由父亲指向孩子,深度沿边递增 1;
| 步骤 | 操作 | 说明 | 输出 |
|---|---|---|---|
| 初始 | — | 权值 1 2 3 4 5 | |
| 1 | 3 3 |
路径 |
6 |
| 2 | 1 2 1 |
||
| 3 | 3 5 |
路径 |
9 |
| 4 | 2 1 2 |
整棵树加 2,权值变 3 5 5 6 7 | |
| 5 | 3 3 |
路径 |
13 |
三次询问的答案依次为 6、9、13:第 4 步整棵树加 2 后,路径上的点数(3 个)就是增量 6 的来源,
正好对应下文公式中的
正解
思路
朴素做法与瓶颈。 单点加直接改
第一步:DFS 序把子树变成连续区间。 按进入时刻给节点编号
第二步:换维护对象——直接维护询问答案
- 单点加(
):受影响的 恰是 ,每个 的答案 统一 —— 区间 加 。修改从"点"升维到"区间",反而变规整了。
第三步:子树加拆成 depth 的一次函数。 给
增量是
第四步:询问退化为点查。 三种操作落到数据结构上的动作汇总如下(区间均为
| 操作 | 对 |
树状数组上的动作 |
|---|---|---|
| 初始权值 |
||
| 1 单点加 |
||
| 2 子树加 |
||
| 3 询问 |
输出 |
三类操作全部统一为"区间加、点查":树状数组按差分实现(左端
代码
Python 版:
#!/usr/bin/env python3
# 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-09-30 18:12
# update_at: 2026-09-30 18:12
import sys
def tour(adj: list[list[int]], root: int) -> tuple[list[int], list[int], list[int]]:
"""迭代 DFS 求欧拉序:返回入点时刻 tin、出点时刻 tout、深度 depth(均为 1 起下标)。
以入点时刻为编号,子树恰好对应连续区间 [tin[u], tout[u]]。
"""
n = len(adj) - 1
tin = [0] * (n + 1)
tout = [0] * (n + 1)
depth = [0] * (n + 1)
t = 0
stack = [(root, 0, False)] # (节点, 父节点, 是否为出栈标记)
while stack:
u, p, exiting = stack.pop()
if exiting:
tout[u] = t # 此时子树内所有节点都已计完入点时刻
continue
t += 1
tin[u] = t
depth[u] = depth[p] + 1 if p else 0 # 根深度记 0
stack.append((u, p, True))
stack += [(v, u, False) for v in adj[u] if v != p]
return tin, tout, depth
def bit_add(bit: list[int], i: int, v: int) -> None:
"""树状数组下标 i 加 v。"""
while i < len(bit):
bit[i] += v
i += i & -i
def bit_sum(bit: list[int], i: int) -> int:
"""树状数组前缀和 Σ[1..i]。"""
s = 0
while i:
s += bit[i]
i -= i & -i
return s
def range_add(bit: list[int], l: int, r: int, v: int) -> None:
"""区间 [l, r] 每个位置加 v:差分写法,左端 +v、右端后一位 −v,点查即前缀和。"""
bit_add(bit, l, v)
bit_add(bit, r + 1, -v)
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, m = next(data), next(data)
weight = [0] + [next(data) for _ in range(n)]
adj: list[list[int]] = [[] for _ in range(n + 1)]
for _ in range(n - 1):
fr, to = next(data), next(data)
adj[fr].append(to)
adj[to].append(fr)
tin, tout, depth = tour(adj, 1)
# 设 A[y] = 根到 y 的路径点权和。每次修改对 A[y] 的贡献要么是常数,
# 要么是 depth[y] 的线性函数,故拆成两棵树状数组:
# A[y] = depth[y] * q1 点查 + q2 点查(区间加、点查)。
q1 = [0] * (n + 2)
q2 = [0] * (n + 2)
# 初始权值:u 的权值让子树内每个 y 的路径和都 +weight[u]
for u in range(1, n + 1):
range_add(q2, tin[u], tout[u], weight[u])
out: list[str] = []
for _ in range(m):
op = next(data)
x = next(data)
l, r = tin[x], tout[x]
if op == 1: # 单点加 a:只影响子树内 y 的路径和,统一 +a
a = next(data)
range_add(q2, l, r, a)
elif op == 2: # 子树加 a:y 的路径和 += a * (子树内祖先个数)
a = next(data) # y 在子树内时为 a*(depth[y]-depth[x]+1),否则为 0
range_add(q1, l, r, a) # depth[y] 的系数 a
range_add(q2, l, r, a * (1 - depth[x])) # 常数项 a*(1-depth[x])
else: # 询问:根到 x 的路径点权和
out.append(str(depth[x] * bit_sum(q1, tin[x]) + bit_sum(q2, tin[x])))
print('\n'.join(out))
if __name__ == "__main__":
solve()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-05 08:25
* update_at: 2026-10-05 08:25
*/
#include <cstdio>
#include <vector>
using namespace std;
typedef long long ll;
// 题目数据规模:N, M <= 1e5
const int MAXN = 100005;
int n, m;
vector<int> g[MAXN]; // 邻接表,g[u] 存与 u 相邻的点(树题用 vector 写法更直观)
ll weight[MAXN]; // weight[u]:节点 u 的初始权值
int tin[MAXN]; // tin[u]:DFS 进入 u 的时刻(欧拉序编号,1 起)
int tout[MAXN]; // tout[u]:u 子树内最后一个点的进入时刻,子树对应 [tin[u], tout[u]]
int depth[MAXN]; // depth[u]:根为 0 的深度,最大不超过 n,用 int 即可
int par[MAXN]; // par[u]:u 的父亲,根的父亲记 0
ll bit1[MAXN]; // 树状数组 1:维护 A[y] 中 depth[y] 的一次项系数
ll bit2[MAXN]; // 树状数组 2:维护 A[y] 中的常数项
int stk[MAXN * 2]; // 迭代 DFS 的显式栈:正数为进入事件,负数为对应节点的退出事件
// 迭代 DFS 求欧拉序与深度:用显式栈代替递归,避免链状树上的爆栈。
// 栈中正数表示进入节点,负数表示该节点子树已全部进入(退出事件)。
void build_tour() {
int top = 0;
int timer = 0;
stk[++top] = 1;
par[1] = 0;
depth[1] = 0;
while (top > 0) {
int u = stk[top--];
if (u < 0) {
tout[-u] = timer; // 退出时,子树内所有点都已编号
continue;
}
tin[u] = ++timer;
stk[++top] = -u; // 先压退出事件,保证它排在所有孩子之后
for (size_t i = 0; i < g[u].size(); i++) {
int v = g[u][i];
if (v == par[u]) continue;
par[v] = u;
depth[v] = depth[u] + 1;
stk[++top] = v;
}
}
}
// 树状数组单点加:下标 i 处加 v。
void bit_add(ll bit[], int i, ll v) {
for (; i <= n + 1; i += i & -i) {
bit[i] += v;
}
}
// 树状数组前缀和 Σ(1..i):在差分写法下就是单点查询。
ll bit_sum(ll bit[], int i) {
ll s = 0;
for (; i > 0; i -= i & -i) {
s += bit[i];
}
return s;
}
// 区间 [l, r] 统一加 v:差分树状数组,左端 +v、右端后一位 -v。
void range_add(ll bit[], int l, int r, ll v) {
bit_add(bit, l, v);
bit_add(bit, r + 1, -v);
}
int main() {
scanf("%d %d", &n, &m);
for (int u = 1; u <= n; u++) {
scanf("%lld", &weight[u]);
}
for (int i = 1; i < n; i++) {
int fr, to;
scanf("%d %d", &fr, &to);
g[fr].push_back(to);
g[to].push_back(fr);
}
build_tour();
// 设 A[y] = 根到 y 的路径点权和。所有修改对 A[y] 的贡献要么是常数,
// 要么是 depth[y] 的一次函数,因此写成 A[y] = depth[y] * bit1 点查 + bit2 点查。
// 初始权值:u 的权值让子树内每个 y 的 A[y] 都 +weight[u],属于常数项。
for (int u = 1; u <= n; u++) {
range_add(bit2, tin[u], tout[u], weight[u]);
}
for (int i = 1; i <= m; i++) {
int op, x;
scanf("%d %d", &op, &x);
int l = tin[x], r = tout[x];
if (op == 1) {
// 单点加 a:只影响子树内每个 y,统一 +a
ll a;
scanf("%lld", &a);
range_add(bit2, l, r, a);
} else if (op == 2) {
// 子树加 a:y 在子树内时 A[y] 增加 a*(depth[y]-depth[x]+1),
// 拆成 depth[y] 的系数 a 与常数项 a*(1-depth[x]) 两段区间加。
ll a;
scanf("%lld", &a);
range_add(bit1, l, r, a);
range_add(bit2, l, r, a * (1 - depth[x]));
} else {
// 询问根到 x 的路径点权和
ll ans = depth[x] * bit_sum(bit1, tin[x]) + bit_sum(bit2, tin[x]);
printf("%lld\n", ans);
}
}
return 0;
}复杂度
- 时间:一次迭代 DFS 求欧拉序
;每个操作 次树状数组更新/查询, 总计 ,与 C++ 正解(DFS 序 + 树状数组、或树链剖分 + 线段树)同阶。 - 空间:
,邻接表、欧拉序数组与两棵树状数组各 。
总结
- 祖先链与子树是树上一对对偶结构:DFS 序让子树变连续,而"祖先链"通过贡献视角
(点
影响整棵子树)被翻译成同一种坐标下的区间操作。 - 子树加的贡献
含有与位置有关的 , 用"一次项 + 常数项"拆成两棵树状数组,是把非统一增量化归为统一区间加的常用手法。 - 最终三类操作全部是"区间加、点查",差分树状数组
完成, 总复杂度 ;Python 实现全部 10 组真实数据 0.4s 内通过。