路径加可以先做树上差分,再把子树和展开成 diff 的加权求和;结合 LCA、DFS 序和两棵树状数组,就能在线维护路径加与子树和查询。
OJ: luogu
题目 ID: P3833
难度:提高+/省选-
标签:LCA树上差分树形结构
日期: 2026-06-20 02:51
题意
给一棵根为 0 的树,初始时每个点的果子数都是 0。
有两类操作:
-
A u v d
把路径u -> v上所有节点的果子数都加上d -
Q u
询问当前以u为根的整棵子树里,一共有多少个果子
样例树
样例树是一条链:
graph G {
0 -- 1;
1 -- 2;
2 -- 3;
}
执行一次 A 1 3 1 后:
- 点
1, 2, 3各加1
所以:
- 子树
0的和是3 - 子树
1的和是3 - 子树
2的和是2
思路
先看一个最直接的小数据暴力:
// brute.cpp:真的把路径上的点都加上 d,再暴力统计子树和。
// 复杂度较高,只适合小数据理解和对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 25;
int n;
vector<int> g[MAXN];
int parent_arr[MAXN];
int depth_arr[MAXN];
long long val[MAXN];
void build_parent_depth() {
vector<int> st;
st.push_back(0);
parent_arr[0] = 0;
depth_arr[0] = 0;
while (!st.empty()) {
int u = st.back();
st.pop_back();
for (size_t i = 0; i < g[u].size(); i++) {
int v = g[u][i];
if (v == parent_arr[u]) {
continue;
}
parent_arr[v] = u;
depth_arr[v] = depth_arr[u] + 1;
st.push_back(v);
}
}
}
int lca(int a, int b) {
while (depth_arr[a] > depth_arr[b]) {
a = parent_arr[a];
}
while (depth_arr[b] > depth_arr[a]) {
b = parent_arr[b];
}
while (a != b) {
a = parent_arr[a];
b = parent_arr[b];
}
return a;
}
void add_path(int u, int v, long long d) {
int p = lca(u, v);
while (u != p) {
val[u] += d;
u = parent_arr[u];
}
while (v != p) {
val[v] += d;
v = parent_arr[v];
}
val[p] += d;
}
long long dfs_sum(int u, int fa) {
long long res = val[u];
for (size_t i = 0; i < g[u].size(); i++) {
int v = g[u][i];
if (v == fa) {
continue;
}
res += dfs_sum(v, u);
}
return res;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 0; i < n; i++) {
g[i].clear();
parent_arr[i] = 0;
depth_arr[i] = 0;
val[i] = 0;
}
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_depth();
int q;
cin >> q;
while (q--) {
char op;
cin >> op;
if (op == 'A') {
int u, v;
long long d;
cin >> u >> v >> d;
add_path(u, v, d);
}
else {
int u;
cin >> u;
cout << dfs_sum(u, parent_arr[u]) << '\n';
}
}
return 0;
}暴力做法就是:
- 真正把路径
u -> v上的每个点都加上d - 查询时再暴力把子树里的点全部加起来
这个做法最贴近题意,但显然扛不住大数据。
这题的关键不是树剖本身,而是先把“路径加”换成“树上差分”。
对于一次路径加 u, v, d,设 p = lca(u, v),做:
diff[u] += ddiff[v] += ddiff[p] -= ddiff[parent(p)] -= d(如果p不是根)
这样做完以后,某个点 x 当前真正的果子数,等于:
diff在x子树里的总和
也就是:
value[x] = sum(diff[y]), y 在 subtree(x)
现在题目要求的是:
subtree(u)里所有value[x]的总和
把这个式子再展开一层:
sum_{x in subtree(u)} value[x]
等于
sum_{y in subtree(u)} diff[y] * (depth[y] - depth[u] + 1)
原因是:
- 一个点
y的差分值,会贡献给它在subtree(u)里的所有祖先 - 这样的祖先数量正好是
depth[y] - depth[u] + 1
于是查询可以改写成:
sum(diff * (depth+1)) - depth[u] * sum(diff)
这就很适合用 DFS 序来维护:
- 子树在 DFS 序上是连续区间
- 用一棵树状数组维护
diff - 再用另一棵树状数组维护
diff * (depth+1)
路径加时只做 4 次单点修改。
查询子树和时,只要在 [tin[u], tout[u]] 上查这两个区间和,再套公式即可。
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100000 + 5;
const int LOG = 18;
struct Fenwick {
int n;
long long bit[MAXN];
void init(int n_) {
n = n_;
for (int i = 1; i <= n; i++) {
bit[i] = 0;
}
}
void add(int x, long long delta) {
for (int i = x; i <= n; i += i & -i) {
bit[i] += delta;
}
}
long long sum_prefix(int x) const {
long long res = 0;
for (int i = x; i > 0; i -= i & -i) {
res += bit[i];
}
return res;
}
long long range_sum(int l, int r) const {
if (l > r) {
return 0;
}
return sum_prefix(r) - sum_prefix(l - 1);
}
};
int n;
vector<int> g[MAXN];
int parent_arr[MAXN];
int depth_arr[MAXN];
int up[MAXN][LOG];
int tin[MAXN], tout[MAXN], dfs_clock;
int iter_idx[MAXN];
Fenwick bit_cnt; // 维护树上差分值 diff
Fenwick bit_depth; // 维护 diff * (depth + 1)
void init_tree(int n) {
dfs_clock = 0;
for (int i = 0; i < n; i++) {
g[i].clear();
parent_arr[i] = 0;
depth_arr[i] = 0;
tin[i] = 0;
tout[i] = 0;
iter_idx[i] = 0;
for (int j = 0; j < LOG; j++) {
up[i][j] = 0;
}
}
}
void add_edge(int u, int v) {
g[u].push_back(v);
g[v].push_back(u);
}
void build_lca_and_dfn() {
vector<int> st;
st.push_back(0);
parent_arr[0] = 0;
depth_arr[0] = 0;
tin[0] = ++dfs_clock;
while (!st.empty()) {
int u = st.back();
if (iter_idx[u] < (int)g[u].size()) {
int v = g[u][iter_idx[u]++];
if (v == parent_arr[u]) {
continue;
}
parent_arr[v] = u;
depth_arr[v] = depth_arr[u] + 1;
up[v][0] = u;
for (int j = 1; j < LOG; j++) {
up[v][j] = up[up[v][j - 1]][j - 1];
}
tin[v] = ++dfs_clock;
st.push_back(v);
}
else {
tout[u] = dfs_clock;
st.pop_back();
}
}
}
int kth_ancestor(int u, int k) {
for (int j = 0; j < LOG; j++) {
if (k & (1 << j)) {
u = up[u][j];
}
}
return u;
}
int lca(int a, int b) {
if (depth_arr[a] < depth_arr[b]) {
swap(a, b);
}
a = kth_ancestor(a, depth_arr[a] - depth_arr[b]);
if (a == b) {
return a;
}
for (int j = LOG - 1; j >= 0; j--) {
if (up[a][j] != up[b][j]) {
a = up[a][j];
b = up[b][j];
}
}
return up[a][0];
}
void add_diff(int u, long long delta) {
int pos = tin[u];
bit_cnt.add(pos, delta);
bit_depth.add(pos, delta * (depth_arr[u] + 1LL));
}
// 对路径 u-v 上所有点加 d。
// 使用树上差分:diff[u] += d, diff[v] += d, diff[lca] -= d, diff[parent(lca)] -= d。
void add_path(int u, int v, long long d) {
int p = lca(u, v);
add_diff(u, d);
add_diff(v, d);
add_diff(p, -d);
if (p != 0) {
add_diff(parent_arr[p], -d);
}
}
// 当前子树和:
// sum_{x in subtree(u)} value[x]
// = sum_{y in subtree(u)} diff[y] * (depth[y] - depth[u] + 1)
// = sum(diff * (depth+1)) - depth[u] * sum(diff)
long long query_subtree(int u) {
long long s1 = bit_cnt.range_sum(tin[u], tout[u]);
long long s2 = bit_depth.range_sum(tin[u], tout[u]);
return s2 - 1LL * depth_arr[u] * s1;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
init_tree(n);
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
add_edge(u, v);
}
build_lca_and_dfn();
bit_cnt.init(n);
bit_depth.init(n);
int q;
cin >> q;
while (q--) {
char op;
cin >> op;
if (op == 'A') {
int u, v;
long long d;
cin >> u >> v >> d;
add_path(u, v, d);
}
else {
int u;
cin >> u;
cout << query_subtree(u) << '\n';
}
}
return 0;
}复杂度
预处理:
- DFS 序和倍增 LCA:
每次操作:
- 路径加:1 次 LCA + 4 次树状数组单点修改,
- 子树查询:2 次树状数组区间求和,
空间复杂度:
总结
这题最值得记住的不是“树状数组”本身,而是这一步转化:
- 路径加先做树上差分
- 子树和再把
value[x]展开成diff的加权和
一旦写成:
sum(diff * (depth+1)) - depth[u] * sum(diff)
整题就从“树上路径更新 + 子树查询”变成了:
LCADFS 序两棵树状数组
的经典组合。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
