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

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

1. 树上差分是什么及其作用
定义

树上差分是一种利用差分思想处理树上路径修改问题的算法。它通过对树上节点的差分数组进行操作,将树上路径的修改问题转化为差分数组的修改问题。

作用
  1. 高效处理路径修改:在树中,对路径上所有节点进行加/减操作的时间复杂度可优化到O(logn)或O(1)
  2. 支持多次修改后统一查询:先进行所有差分操作,最后通过一次DFS计算出所有节点的最终值
  3. 解决多种问题
    • 树上点权修改、查询
    • 树上边权修改、查询
    • 覆盖次数统计
    • 最近公共祖先(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
操作流程
  1. 预处理LCA(最近公共祖先)所需信息
  2. 进行差分操作
  3. 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遍历完成所有节点的值计算。

适用场景
  1. 静态树:树的结构不会改变
  2. 批量修改:需要多次路径修改操作
  3. 最后查询:所有修改完成后才需要查询节点值
算法复杂度
  • 时间复杂度:O((n+m)logn),其中n为节点数,m为操作数(使用倍增LCA)
  • 空间复杂度:O(nlogn)(存储倍增数组)
注意事项
  1. 根节点的选择:需要明确树的根节点,一般设为1
  2. LCA算法选择:可根据数据规模选择倍增法、树链剖分或Tarjan算法
  3. 边界处理:注意根节点的父节点处理,避免越界
  4. 差分类型选择:根据问题是点权还是边权选择相应的差分公式
扩展应用
  1. 结合树链剖分:可处理动态树上的路径修改
  2. 树上二分答案:结合差分判断可行性
  3. 覆盖问题:统计每条边/每个点被路径覆盖的次数
  4. 最近公共祖先应用:解决需要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;
}
相关推荐
Aevget4 小时前
MFC扩展库BCGControlBar Pro v37.2新版亮点:控件功能进一步升级
c++·mfc·界面控件
Tansmjs4 小时前
C++与GPU计算(CUDA)
开发语言·c++·算法
挖矿大亨6 小时前
c++中的函数模版
java·c++·算法
阿基米东6 小时前
基于 C++ 的机器人软件框架(具身智能)开源通信库选型分析
c++·机器人·开源
偷星星的贼116 小时前
C++中的对象池模式
开发语言·c++·算法
CN-Dust6 小时前
【C++】洛谷P3073 [USACO13FEB] Tractor S
开发语言·c++
2401_829004026 小时前
C++中的适配器模式变体
开发语言·c++·算法
平生不喜凡桃李7 小时前
二叉树遍历非递归写法: 栈
c++··二叉树遍历·非递归
-To be number.wan7 小时前
算法学习日记 | 枚举
c++·学习·算法
CSDN_RTKLIB7 小时前
多线程开篇记录几个例子
c++