树上前缀和与差分

树上前缀和与差分

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

例题:
https://oi-wiki.org/basic/prefix-sum/#例题_1

image
image

image

详细解释 树上前缀和与树上差分

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去加。

于是——我们引入差分思想,在树上“打标记”,最后只遍历一次整棵树,就能知道每个点最终的值变化。


三、树上点差分:路径加值操作

操作目标:

对树上两点 uv 之间的路径上的所有都加上 +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)uv 的最近公共祖先

  • 所以整条路径就是:根到 u + 根到 v - 根到公共祖先的路径 * 2


🛠 步骤拆解

  1. 建树(存边 + 边权)

  2. DFS 预处理 dis[] 和深度 depth[]

  3. 倍增法预处理 LCA

  4. 用公式计算距离


✅ 代码实现(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)
posted @ 2025-05-20 17:58  katago  阅读(130)  评论(0)    收藏  举报