树上差分CSP-S

目录

💡 核心思想

树上差分是处理"树上路径统计"问题的技巧。对于路径 u→v,可以拆分为 u→root + v→root - 2×lca→root。通过对节点进行差分标记(u 和 v 处 +1,lca 和 lca 的父节点处 -1),最后一次 DFS 求子树和即可得到每个节点被经过的次数。

树上差分是 LCA 的标准搭档。没有 LCA,树上差分就失去了根基。很多选手看到"统计每条边被多少路径覆盖"就想到线段树或树链剖分,其实一个差分数组 + 一次 DFS 就搞定了。记住标记公式:路径 u→v 经过的所有节点,在 u 和 v 处 +1,在 lca 和 fa[lca] 处 -1。

🎯 直觉理解

想象你在树上撒面包屑:从 u 走到 v,你在 u 和 v 各放一袋面包屑,在它们的最近公共祖先 lca 处拿走两袋(因为 u→lca 和 v→lca 的面包屑在 lca 处重叠了)。然后让所有面包屑顺着树往下流,每个节点最终收到的面包屑数量就是它所在路径的数量。

📝 算法流程

  1. 预处理 LCA(倍增或 Tarjan)
  2. 对于每条路径 (u, v):diff[u]++, diff[v]++, diff[lca]--, diff[fa[lca]]--
  3. 从根节点开始 DFS,求子树和:sum[u] = diff[u] + Σ sum[v](v 是 u 的子节点)
  4. sum[u] 即为经过节点 u 的路径数
  5. 若统计边,则将标记放在边上,最后看子节点向父节点的边

$$\text{路径 }u \to v: \quad diff[u]++,\ diff[v]++,\ diff[lca]-=2,\ diff[fa[lca]] \text{(可选)}$$

📊 复杂度分析

指标复杂度
时间$O((n+q) \log n)$(含 LCA 预处理)
空间$O(n)$

💻 参考实现(C++)

C++ (C++17)
#include 
using namespace std;

const int N = 100005;
vector g[N];
int diff[N], ans[N];

void dfs(int u, int f) {
    for (int v : g[u]) {
        if (v == f) continue;
        dfs(v, u);
        diff[u] += diff[v]; // 子树和累加到父节点
    }
    ans[u] = diff[u];
}

int main() {
    int n = 5;
    g[1].push_back(2); g[2].push_back(1);
    g[1].push_back(3); g[3].push_back(1);
    g[2].push_back(4); g[4].push_back(2);
    g[2].push_back(5); g[5].push_back(2);
    
    // 路径 4->5:lca=2
    diff[4]++; diff[5]++; diff[2] -= 2;
    
    dfs(1, 0);
    for (int i = 1; i <= n; i++)
        cout << "节点 " << i << " 被 " << ans[i] << " 条路径覆盖" << endl;
    return 0;
}

⚠️ 常见坑点

LCA 预处理错误——树上差分依赖正确的 LCA

忘记处理根节点——如果 lca 是根,fa[lca] 不存在

diff 数组初始化——每次查询前要清零

与点差分和边差分混淆——边差分需要对子节点的值判断

📚 相关题目

题目来源难度备注
P3128 最大流洛谷CSP-S树上差分模板题
P2680 运输计划洛谷CSP-S树上差分 + 二分答案
P1600 天天爱跑步洛谷CSP-S树上差分综合题