信奥赛C++提高组csp-s之树上差分

1. 树上差分是什么及其作用
定义
树上差分是一种利用差分思想处理树上路径修改问题的算法。它通过对树上节点的差分数组进行操作,将树上路径的修改问题转化为差分数组的修改问题。
作用
- 高效处理路径修改:在树中,对路径上所有节点进行加/减操作的时间复杂度可优化到O(logn)或O(1)
- 支持多次修改后统一查询:先进行所有差分操作,最后通过一次DFS计算出所有节点的最终值
- 解决多种问题 :
- 树上点权修改、查询
- 树上边权修改、查询
- 覆盖次数统计
- 最近公共祖先(LCA)相关应用
2. 树上差分原理
差分思想回顾
在一维数组中,差分数组diff[i] = arr[i] - arr[i-1],区间[l, r]加k可转化为diff[l] += k, diff[r+1] -= k
树上差分分类
1. 点权差分
对路径u→v上所有点权值加k:
cpp
diff[u] += k
diff[v] += k
diff[lca] -= k
diff[father[lca]] -= k // lca的父节点
2. 边权差分
对路径u→v上所有边权值加k(将边权映射到深度较大的端点):
cpp
diff[u] += k
diff[v] += k
diff[lca] -= 2*k
操作流程
- 预处理LCA(最近公共祖先)所需信息
- 进行差分操作
- DFS累加差分值得到最终结果
3. 研究案例1:Max Flow P
题目描述
Farmer John 在他的谷仓中安装了 N − 1 N-1 N−1 条管道,用于在 N N N 个牛棚之间运输牛奶( 2 ≤ N ≤ 50 , 000 2 \leq N \leq 50,000 2≤N≤50,000),牛棚方便地编号为 1 ... N 1 \ldots N 1...N。每条管道连接一对牛棚,所有牛棚通过这些管道相互连接。
FJ 正在 K K K 对牛棚之间泵送牛奶( 1 ≤ K ≤ 100 , 000 1 \leq K \leq 100,000 1≤K≤100,000)。对于第 i i i 对牛棚,你被告知两个牛棚 s i s_i si 和 t i t_i ti,这是牛奶以单位速率泵送的路径的端点。FJ 担心某些牛棚可能会因为过多的牛奶通过它们而不堪重负,因为一个牛棚可能会作为许多泵送路径的中转站。请帮助他确定通过任何一个牛棚的最大牛奶量。如果牛奶沿着从 s i s_i si 到 t i t_i ti 的路径泵送,那么它将被计入端点牛棚 s i s_i si 和 t i t_i ti,以及它们之间路径上的所有牛棚。
输入格式
输入的第一行包含 N N N 和 K K K。
接下来的 N − 1 N-1 N−1 行每行包含两个整数 x x x 和 y y y( x ≠ y x \ne y x=y),描述连接牛棚 x x x 和 y y y 的管道。
接下来的 K K K 行每行包含两个整数 s s s 和 t t t,描述牛奶泵送路径的端点牛棚。
输出格式
输出一个整数,表示通过谷仓中任何一个牛棚的最大牛奶量。
输入样例
5 10
3 4
1 5
4 2
5 4
5 4
5 4
3 5
4 3
4 3
1 3
3 5
5 4
1 5
3 4
输出样例
9
说明/提示
2 ≤ N ≤ 5 × 10 4 , 1 ≤ K ≤ 10 5 2 \le N \le 5 \times 10^4,1 \le K \le 10^5 2≤N≤5×104,1≤K≤105。
问题分析
- 题目:给一棵树,有k次操作,每次将节点u到节点v路径上所有节点权值+1,求最终权值最大的节点
- 思路:使用点权差分,每次操作在差分数组上修改,最后DFS累加
代码实现
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 50005;
const int LOG = 20; // log2(50000) ≈ 16
vector<int> graph[MAXN]; // 邻接表存储树
int depth[MAXN]; // 节点深度
int parent[MAXN][LOG]; // parent[u][i]表示u的2^i级祖先
int diff[MAXN]; // 差分数组
int n, k, ans = 0;
// DFS预处理深度和倍增数组
void dfs_pre(int u, int fa) {
depth[u] = depth[fa] + 1;
parent[u][0] = fa;
// 预处理倍增数组
for (int i = 1; i < LOG; i++) {
parent[u][i] = parent[parent[u][i-1]][i-1];
}
// 遍历子节点
for (int v : graph[u]) {
if (v != fa) {
dfs_pre(v, u);
}
}
}
// 求LCA(最近公共祖先)
int lca(int u, int v) {
// 确保u是深度较大的节点
if (depth[u] < depth[v]) swap(u, v);
// 将u跳到与v同一深度
for (int i = LOG-1; i >= 0; i--) {
if (depth[parent[u][i]] >= depth[v]) {
u = parent[u][i];
}
}
// 如果已经相同,直接返回
if (u == v) return u;
// 同时上跳
for (int i = LOG-1; i >= 0; i--) {
if (parent[u][i] != parent[v][i]) {
u = parent[u][i];
v = parent[v][i];
}
}
// 返回LCA
return parent[u][0];
}
// DFS累加差分值,计算最终权值
void dfs_sum(int u, int fa) {
for (int v : graph[u]) {
if (v != fa) {
dfs_sum(v, u);
diff[u] += diff[v]; // 子节点差分值累加到父节点
}
}
ans = max(ans, diff[u]); // 更新最大值
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
// 输入
cin >> n >> k;
// 建树
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
graph[u].push_back(v);
graph[v].push_back(u);
}
// 预处理
depth[0] = -1; // 根节点的父节点深度设为-1
dfs_pre(1, 0); // 假设1为根节点
// k次操作
for (int i = 0; i < k; i++) {
int u, v;
cin >> u >> v;
int p = lca(u, v); // 求LCA
// 点权差分操作
diff[u]++;
diff[v]++;
diff[p]--;
if (parent[p][0] != 0) { // 如果LCA不是根节点
diff[parent[p][0]]--;
}
}
// 计算最终结果
dfs_sum(1, 0);
// 输出
cout << ans << endl;
return 0;
}
4. 研究案例2:松鼠的新家
题目描述
松鼠的新家是一棵树,前几天刚刚装修了新家,新家有 n n n 个房间,并且有 n − 1 n-1 n−1 根树枝连接,每个房间都可以相互到达,且俩个房间之间的路线都是唯一的。天哪,他居然真的住在"树"上。
松鼠想邀请小熊维尼前来参观,并且还指定一份参观指南,他希望维尼能够按照他的指南顺序,先去 a 1 a_1 a1,再去 a 2 a_2 a2,......,最后到 a n a_n an,去参观新家。可是这样会导致重复走很多房间,懒惰的维尼不停地推辞。可是松鼠告诉他,每走到一个房间,他就可以从房间拿一块糖果吃。
维尼是个馋家伙,立马就答应了。现在松鼠希望知道为了保证维尼有糖果吃,他需要在每一个房间各放至少多少个糖果。
因为松鼠参观指南上的最后一个房间 a n a_n an 是餐厅,餐厅里他准备了丰盛的大餐,所以当维尼在参观的最后到达餐厅时就不需要再拿糖果吃了。
输入格式
第一行一个正整数 n n n,表示房间个数第二行 n n n 个正整数,依次描述 a 1 , a 2 , ⋯ , a n a_1, a_2,\cdots,a_n a1,a2,⋯,an。
接下来 n − 1 n-1 n−1 行,每行两个正整数 x , y x,y x,y,表示标号 x x x 和 y y y 的两个房间之间有树枝相连。
输出格式
一共 n n n 行,第 i i i 行输出标号为 i i i 的房间至少需要放多少个糖果,才能让维尼有糖果吃。
输入样例
5
1 4 5 3 2
1 2
2 4
2 3
4 5
输出样例
1
2
1
2
1
说明/提示
对于全部的数据, 2 ≤ n ≤ 3 × 10 5 2 \le n \le 3 \times 10^5 2≤n≤3×105, 1 ≤ a i ≤ n 1 \le a_i \le n 1≤ai≤n。
问题分析
- 题目:松鼠按顺序访问n个房间(树上的节点),访问路径上所有节点(包括起点终点)糖果数+1
- 特殊点:除起点外,每个节点作为终点时被重复计算了1次
- 思路:点权差分,最后对重复计算的节点进行修正
代码实现
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 300005;
const int LOG = 20;
vector<int> graph[MAXN];
int depth[MAXN], parent[MAXN][LOG];
int diff[MAXN], order[MAXN]; // order存储访问顺序
int n;
// 预处理深度和倍增数组
void dfs_pre(int u, int fa) {
depth[u] = depth[fa] + 1;
parent[u][0] = fa;
for (int i = 1; i < LOG; i++) {
parent[u][i] = parent[parent[u][i-1]][i-1];
}
for (int v : graph[u]) {
if (v != fa) {
dfs_pre(v, u);
}
}
}
// 求LCA
int lca(int u, int v) {
if (depth[u] < depth[v]) swap(u, v);
for (int i = LOG-1; i >= 0; i--) {
if (depth[parent[u][i]] >= depth[v]) {
u = parent[u][i];
}
}
if (u == v) return u;
for (int i = LOG-1; i >= 0; i--) {
if (parent[u][i] != parent[v][i]) {
u = parent[u][i];
v = parent[v][i];
}
}
return parent[u][0];
}
// 累加差分值,得到最终糖果数
void dfs_sum(int u, int fa) {
for (int v : graph[u]) {
if (v != fa) {
dfs_sum(v, u);
diff[u] += diff[v];
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
// 输入
cin >> n;
// 读入访问顺序
for (int i = 1; i <= n; i++) {
cin >> order[i];
}
// 建树
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
graph[u].push_back(v);
graph[v].push_back(u);
}
// 预处理
depth[0] = -1;
dfs_pre(1, 0);
// 处理访问路径
for (int i = 1; i < n; i++) {
int u = order[i], v = order[i+1];
int p = lca(u, v);
// 点权差分操作
diff[u]++;
diff[v]++;
diff[p]--;
if (parent[p][0] != 0) {
diff[parent[p][0]]--;
}
}
// 累加得到初步结果
dfs_sum(1, 0);
// 修正:除起点外,每个节点作为终点时被多算了一次
for (int i = 2; i <= n; i++) {
diff[order[i]]--;
}
// 输出结果
for (int i = 1; i <= n; i++) {
cout << diff[i] << "\n";
}
return 0;
}
5. 总结树上差分
核心思想
将树上路径的批量修改问题转化为差分数组的局部修改问题,通过一次DFS遍历完成所有节点的值计算。
适用场景
- 静态树:树的结构不会改变
- 批量修改:需要多次路径修改操作
- 最后查询:所有修改完成后才需要查询节点值
算法复杂度
- 时间复杂度:O((n+m)logn),其中n为节点数,m为操作数(使用倍增LCA)
- 空间复杂度:O(nlogn)(存储倍增数组)
注意事项
- 根节点的选择:需要明确树的根节点,一般设为1
- LCA算法选择:可根据数据规模选择倍增法、树链剖分或Tarjan算法
- 边界处理:注意根节点的父节点处理,避免越界
- 差分类型选择:根据问题是点权还是边权选择相应的差分公式
扩展应用
- 结合树链剖分:可处理动态树上的路径修改
- 树上二分答案:结合差分判断可行性
- 覆盖问题:统计每条边/每个点被路径覆盖的次数
- 最近公共祖先应用:解决需要LCA信息的路径问题
更多系列知识,请查看专栏:《信奥赛C++提高组csp-s知识详解及案例实践》:
https://blog.csdn.net/weixin_66461496/category_13113932.html
各种学习资料,助力大家一站式学习和提升!!!
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
cout<<"########## 一站式掌握信奥赛知识! ##########";
cout<<"############# 冲刺信奥赛拿奖! #############";
cout<<"###### 课程购买后永久学习,不受限制! ######";
return 0;
}
1、csp信奥赛高频考点知识详解及案例实践:
CSP信奥赛C++动态规划:
https://blog.csdn.net/weixin_66461496/category_13096895.html点击跳转
CSP信奥赛C++标准模板库STL:
https://blog.csdn.net/weixin_66461496/category_13108077.html 点击跳转
信奥赛C++提高组csp-s知识详解及案例实践:
https://blog.csdn.net/weixin_66461496/category_13113932.html
2、csp信奥赛冲刺一等奖有效刷题题解:
CSP信奥赛C++初赛及复赛高频考点真题解析(持续更新):https://blog.csdn.net/weixin_66461496/category_12808781.html 点击跳转
CSP信奥赛C++一等奖通关刷题题单及题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12673810.html 点击跳转
3、GESP C++考级真题题解:

GESP(C++ 一级+二级+三级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12858102.html 点击跳转

GESP(C++ 四级+五级+六级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12869848.html 点击跳转

GESP(C++ 七级+八级)真题题解(持续更新):
https://blog.csdn.net/weixin_66461496/category_13117178.html
4、CSP信奥赛C++竞赛拿奖视频课:
https://edu.csdn.net/course/detail/40437 点击跳转

· 文末祝福 ·
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
cout<<"跟着王老师一起学习信奥赛C++";
cout<<" 成就更好的自己! ";
cout<<" csp信奥赛一等奖属于你! ";
return 0;
}