树状数组-区间修改-区间查询

GitHub跳转原题关系图返回列表

通过维护差分数组的两个特定系数的树状数组,实现在 $O(\log n)$ 的时间复杂度内高效进行区间的加法修改与求和查询操作。

OJ: roj

题目 ID: 5004

难度:提高

标签:树状数组差分数据结构

创建: 2026-10-08 11:10

更新: 2026-10-08 11:10

形式化题目

给定一个长度为 nn 的数组 a1,a2,…,ana_1, a_2, \ldots, a_n,要求支持以下两种操作,共 mm 次:

  1. 区间修改:给定 x,y,zx, y, z,对所有 i∈[x,y]i \in [x, y],将 aia_i 加上 zz。
  2. 区间查询:给定 x,yx, y,求 ∑i=xyai\sum_{i=x}^y a_i 并输出。

数据范围:1≤n≤1051 \le n \le 10^5,−1000≤ai,z≤1000-1000 \le a_i, z \le 1000。由于数组和可能较大,答案需要使用 64 位整数(long long)。

正解

思路

如果直接在原数组上进行操作,无论是修改还是查询,单次最坏的时间复杂度均为 O(n)O(n)。在 n,m≤105n, m \le 10^5 的数据范围内会超时。

引入差分数组 di=ai−ai−1d_i = a_i - a_{i-1},其中 a0=0a_0 = 0。则原数组中的元素 aia_i 可以表示为:

ai=∑j=1idj a_i = \sum_{j=1}^i d_j
此时,区间修改操作“对 a[x…y]a[x \dots y] 加上 zz”可以直接转化为对差分数组的两个单点修改:
dx←dx+z d_x \gets d_x + z
dy+1←dy+1−z d_{y+1} \gets d_{y+1} - z

进而考虑区间查询操作,我们需要求前缀和 Sx=∑i=1xaiS_x = \sum_{i=1}^x a_i:

Sx=∑i=1xai=∑i=1x∑j=1idj=d1×x+d2×(x−1)+⋯+dx×1=∑i=1xdi×(x−i+1)=(x+1)∑i=1xdi−∑i=1xi⋅di \begin{aligned} S_x &= \sum_{i=1}^x a_i = \sum_{i=1}^x \sum_{j=1}^i d_j \\ &= d_1 \times x + d_2 \times (x-1) + \dots + d_x \times 1 \\ &= \sum_{i=1}^x d_i \times (x - i + 1) \\ &= (x + 1) \sum_{i=1}^x d_i - \sum_{i=1}^x i \cdot d_i \end{aligned}

从上式可知,只需要使用两个树状数组,分别维护:

  1. bit_diff 维护 did_i 的前缀和;
  2. bit_weighted 维护 i⋅dii \cdot d_i 的前缀和。

每次更新 did_i 时,同步在这两个树状数组上做单点修改,注意权值树状数组在增加时乘以的系数是原始下标 ii。区间求和 ∑i=xyai\sum_{i=x}^y a_i 即为 Sy−Sx−1S_y - S_{x-1}。这使得两种操作均能降至 O(log⁡n)O(\log n)。

代码

python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-08 10:37
# update_at: 2026-10-08 11:02

# 区间加 + 区间求和。记差分 d_i = a_i - a_{i-1}(a_0 = 0),则
#   [x,y] 加 z  <=>  d_x += z、d_{y+1} -= z
#   前缀和 S_x = Σ a_i = Σ d_i·(x-i+1) = (x+1)·Σd_i - Σ i·d_i
# 于是用两棵树状数组分别维护 d_i 与 i·d_i,两种操作都降到 O(log n)。

import sys


def point_add(tree_d: list[int], tree_w: list[int], size: int, idx: int, val: int) -> None:
    """在差分下标 idx 处加 val:两棵树同步向上跳,权值树存 i·d_i。"""
    weighted = val * idx  # 权重是「原始下标」,不是树上的节点编号,别写成 val * i
    i = idx
    while i <= size:
        tree_d[i] += val
        tree_w[i] += weighted
        i += i & -i


def prefix_sum(tree_d: list[int], tree_w: list[int], idx: int) -> int:
    """下标 1..idx 的元素和 S_idx = (idx+1)·Σd_i - Σ i·d_i。"""
    sum_d = sum_w = 0
    i = idx
    while i > 0:
        sum_d += tree_d[i]
        sum_w += tree_w[i]
        i -= i & -i
    return (idx + 1) * sum_d - sum_w


def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    n, m = next(data), next(data)

    tree_d = [0] * (n + 2)
    tree_w = [0] * (n + 2)

    # 边读入原数组边转成差分:d_i = a_i - a_{i-1},a_0 视为 0
    last = 0
    for i in range(1, n + 1):
        a = next(data)
        point_add(tree_d, tree_w, n, i, a - last)
        last = a

    out: list[str] = []
    for _ in range(m):
        op = next(data)
        x, y = next(data), next(data)
        if op == 1:
            z = next(data)
            point_add(tree_d, tree_w, n, x, z)
            point_add(tree_d, tree_w, n, y + 1, -z)  # y+1 越界时 while 不执行,相当于只改 d_x
        else:
            out.append(str(prefix_sum(tree_d, tree_w, y) - prefix_sum(tree_d, tree_w, x - 1)))

    if out:
        print('\n'.join(out))


if __name__ == "__main__":
    solve()
cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-08 10:57
 * update_at: 2026-10-08 10:57
 */
// main.cpp:区间加 + 区间求和,用「差分 + 双树状数组」在 O(log n) 内完成两种操作。
//
// 记差分 d_i = a_i - a_{i-1}(a_0 = 0)。区间加 [x,y] 加 z 变成两个单点修改:
// d_x += z、d_{y+1} -= z;区间查询则要把前缀和用差分表示:
//   S_x = Σ_{i=1}^{x} a_i = Σ_{i=1}^{x} d_i·(x-i+1) = (x+1)·Σ d_i - Σ i·d_i
// 所以只要两棵树状数组分别维护 d_i 与 i·d_i,即可 O(log n) 求任意前缀和。

#include <iostream>
using namespace std;

typedef long long ll;

const int MAXN = 100005; // 题目 n <= 1e5

int n, m;
ll bit_diff[MAXN];     // 树状数组一:维护差分 d_i
ll bit_weighted[MAXN]; // 树状数组二:维护 i·d_i

// 在差分 d 的下标 idx 处加 val:两棵树同时向上更新,权值树要带上 idx 这个系数
void add_point(int idx, ll val) {
    for (int i = idx; i <= n; i += i & -i) {
        bit_diff[i] += val;
        bit_weighted[i] += val * idx;
    }
}

// 前 idx 项的和:S_idx = (idx+1)·Σd_i - Σ i·d_i(两棵树一起向下跳)
ll prefix_sum(int idx) {
    ll sum_d = 0, sum_w = 0;
    for (int i = idx; i > 0; i -= i & -i) {
        sum_d += bit_diff[i];
        sum_w += bit_weighted[i];
    }
    return (idx + 1) * sum_d - sum_w;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> m;

    // 边读入原数组边转成差分:a_i - a_{i-1},a_0 视为 0
    ll last = 0;
    for (int i = 1; i <= n; i++) {
        ll a;
        cin >> a;
        add_point(i, a - last);
        last = a;
    }

    for (int q = 0; q < m; q++) {
        int op, x, y;
        cin >> op >> x >> y;
        if (op == 1) {
            ll z;
            cin >> z;
            add_point(x, z);
            add_point(y + 1, -z); // y+1 超过 n 时循环条件直接跳过,相当于不加
        } else {
            cout << prefix_sum(y) - prefix_sum(x - 1) << '\n';
        }
    }

    return 0;
}

复杂度

  • 时间复杂度:无论是区间修改还是区间查询,只需要在树状数组上单次向上或向下跳跃,单次操作时间复杂度为 O(log⁡n)O(\log n)。总时间复杂度为 O(nlog⁡n+mlog⁡n)O(n \log n + m \log n)。
  • 空间复杂度:需要维护两个长度为 n+2n+2 的树状数组(或全局数组),空间复杂度为 O(n)O(n)。

总结

  • 为何使用差分与双树状数组:这题由于既有区间加法操作,又有区间求和查询。普通的单个树状数组只能做到单点修改+区间求和,或者结合差分做到区间修改+单点查询。为了实现“区间修改+区间求和”,必须将前缀和表达式用差分数组展开,推导出需要维护的 ∑di\sum d_i 以及 ∑i⋅di\sum i \cdot d_i。
  • 权值系数必须是原始下标:在给第二个树状数组(维护 i⋅dii \cdot d_i)加值的时候,增加的具体数值应为 val * idx(其中 idx 为操作的原始目标下标),而不是树状数组向后跳跃更新时的节点下标 i。
  • 需要用 long long 的必要性:树状数组维护的累加和尤其是 i⋅dii \cdot d_i 很容易超过 32 位带符号整型(int)的范围。为了防止溢出,求和变量以及参数等处都需要使用 long long(Python 自带大整数,C++ 则需注意类型定义)。
  • 关于原题描述与测试数据:
    • 题面中的原输出格式描述(“全部过河的最少耗时”)是从别处复制遗留的错误,需要通过样例推断真实意图。
    • 本题 data/ 测试数据原先由带有 scanf("%d%d%d") 错误格式符读取 long long 的标程生成,导致“如果第一个操作刚好是区间加,更新会被因垃圾高位值越界而静默丢弃”。该问题已被修复。