树上前缀和与差分
树上前缀和与差分
https://oi-wiki.org/basic/prefix-sum/#树上前缀和
https://oi-wiki.org/basic/prefix-sum/#树上差分
例题:
https://oi-wiki.org/basic/prefix-sum/#例题_1



详细解释 树上前缀和与树上差分
Response:
我们来详细讲解两个重要的树上算法思想:
🌳 树上前缀和(树上前缀路径和)
一、什么是树上前缀和?
树上前缀和是指:
从根节点出发到某个节点
u的路径上所有节点值(或边权)的和。
就像一维数组的前缀和是 sum[i] = a[1] + a[2] + ... + a[i],
树上的前缀和是 sum[u] = a[root] + a[父亲] + ... + a[u]
二、怎么求树上前缀和?
使用 DFS(深度优先搜索),在遍历时顺便累计路径上的值。
✅ 示例代码(节点值的前缀和):
#include <iostream>
#include <vector>
using namespace std;
const int N = 1e5 + 10;
vector<int> tree[N];
int a[N]; // 每个节点的权值
int sum[N]; // sum[u] 表示从根到 u 的路径上节点权值之和
void dfs(int u, int fa) {
sum[u] = sum[fa] + a[u]; // 当前节点的路径和 = 父节点路径和 + 当前节点值
for (int v : tree[u]) {
if (v == fa) continue;
dfs(v, u);
}
}
int main() {
int n;
cin >> n;
// 读入节点权值
for (int i = 1; i <= n; ++i) cin >> a[i];
// 构建树
for (int i = 1; i < n; ++i) {
int u, v;
cin >> u >> v;
tree[u].push_back(v);
tree[v].push_back(u);
}
dfs(1, 0); // 从根节点1开始DFS
// 输出每个节点的前缀和
for (int i = 1; i <= n; ++i)
cout << "sum[" << i << "] = " << sum[i] << endl;
return 0;
}
🌳 树上差分(树上点差分)
一、普通差分回顾
一维数组差分:
如果我们要对 [l, r] 这一段的数同时加上一个值 +x,用差分数组写:
diff[l] += x;
diff[r+1] -= x;
然后最后前缀和一遍,就还原出原数组的值。
二、树上差分的目的
我们要对树上某一段路径上的所有节点或边统一加上/减去某个值。
但如果路径很多,我们不能每次都暴力DFS去加。
于是——我们引入差分思想,在树上“打标记”,最后只遍历一次整棵树,就能知道每个点最终的值变化。
三、树上点差分:路径加值操作
操作目标:
对树上两点 u 和 v 之间的路径上的所有点都加上 +x
差分方法:
我们在三个点打标记:
diff[u] += x;
diff[v] += x;
diff[lca(u,v)] -= x;
diff[parent[lca(u,v)]] -= x;
为什么这样做?
-
差分的含义是:“我会在DFS的时候传播值”
-
所以,我们在两端打上
+x,但在LCA和它的父亲处抵消掉“重复加”的部分。
📌 注意:你需要支持求 LCA(u, v) 和 parent[u],可以用 Tarjan 或 倍增法来实现。
四、最后一步:DFS传播差分
void dfs_diff(int u, int fa) {
for (int v : tree[u]) {
if (v == fa) continue;
dfs_diff(v, u);
diff[u] += diff[v]; // 子树向上传递差分值
}
}
✅ 简化例子(树上点差分):
// 假设我们已经有了LCA和parent数组
// 对u到v路径上的所有点加x
void add_path(int u, int v, int x) {
int anc = lca(u, v);
diff[u] += x;
diff[v] += x;
diff[anc] -= x;
if (parent[anc]) diff[parent[anc]] -= x;
}
树上前缀和 vs 树上差分 总结表:
| 对比项 | 树上前缀和 | 树上差分 |
|---|---|---|
| 用途 | 查询一条路径的累加值 | 多次修改路径上的值后统一统计 |
| 操作方式 | DFS一次直接累加 | 差分打标记 + DFS累计 |
| 修改效率 | O(n) 修改一条路径(暴力) | O(1) 标记后 DFS 一次解决 |
| 查询效率 | O(1) 查询某个点的前缀和 | 需要DFS处理后得到实际值 |
Response:
树上经典问题之一:求树上两点的距离。
📌 题目描述(经典形式)
给定一棵树(n 个节点,n-1 条边),每条边有权值,支持多次查询:
节点
u到节点v之间的最短路径长度(即两点之间路径上所有边权之和)
✅ 解法思路:LCA + 树上前缀和
在树上,两点之间的路径等于:
dist(u, v) = dis[u] + dis[v] - 2 * dis[lca(u, v)]
其中:
-
dis[u]表示从根节点到节点u的路径边权和(树上前缀和) -
lca(u, v)是u和v的最近公共祖先 -
所以整条路径就是:根到 u + 根到 v - 根到公共祖先的路径 * 2
🛠 步骤拆解
-
建树(存边 + 边权)
-
DFS 预处理 dis[] 和深度 depth[]
-
倍增法预处理 LCA
-
用公式计算距离
✅ 代码实现(C++ ACM风格,倍增LCA)
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
const int N = 1e5 + 10;
const int LOG = 20;
struct Edge {
int to, w;
};
vector<Edge> tree[N];
int n, q;
int depth[N]; // 深度
int fa[N][LOG]; // 倍增祖先
long long dis[N]; // 根到每个点的路径和(树上前缀和)
void dfs(int u, int f) {
fa[u][0] = f;
for (int i = 1; i < LOG; ++i)
fa[u][i] = fa[fa[u][i-1]][i-1];
for (auto e : tree[u]) {
int v = e.to, w = e.w;
if (v == f) continue;
depth[v] = depth[u] + 1;
dis[v] = dis[u] + w; // 前缀和
dfs(v, u);
}
}
int lca(int u, int v) {
if (depth[u] < depth[v]) swap(u, v);
// 把 u 提到和 v 同一深度
for (int i = LOG - 1; i >= 0; --i) {
if (depth[fa[u][i]] >= depth[v])
u = fa[u][i];
}
if (u == v) return u;
// 一起跳
for (int i = LOG - 1; i >= 0; --i) {
if (fa[u][i] != fa[v][i]) {
u = fa[u][i];
v = fa[v][i];
}
}
return fa[u][0];
}
long long get_dist(int u, int v) {
int anc = lca(u, v);
return dis[u] + dis[v] - 2 * dis[anc];
}
int main() {
cin >> n >> q;
for (int i = 1; i < n; ++i) {
int u, v, w;
cin >> u >> v >> w;
tree[u].push_back({v, w});
tree[v].push_back({u, w});
}
dfs(1, 0); // 以 1 为根节点
while (q--) {
int u, v;
cin >> u >> v;
cout << get_dist(u, v) << '\n';
}
return 0;
}
📘 示例输入:
5 3
1 2 3
1 3 2
2 4 4
2 5 1
4 5
3 4
2 3
📘 示例输出:
5 // 4→2→5, 路径边权和为 4+1=5
9 // 4→2→1→3, 路径为 4+3+2=9
5 // 2→1→3, 路径为 3+2=5
📌 总结表
| 名称 | 值 |
|---|---|
dis[u] |
根节点到 u 的距离(树上前缀和) |
lca(u,v) |
最近公共祖先 |
dist(u,v) |
dis[u] + dis[v] - 2*dis[lca(u,v)] |
| 查询复杂度 | O(logN) |
| 预处理复杂度 | O(NlogN) |

浙公网安备 33010602011771号