「一本通 4.3 例 2」A Simple Problem with Integers

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

使用两个树状数组分别维护差分序列及其加权和,将区间加与区间查询均优化至对数复杂度。

OJ: roj

题目 ID: 1548

难度:提高

标签:树状数组差分前缀和

创建: 2026-09-30 17:30

更新: 2026-10-07 11:01

形式化题目

给定长度为 nn 的整数序列 a[1..n]a[1..n],要求支持 qq 次操作:

  1. 1 l r x:对所有 i∈[l,r]i \in [l, r],令 a[i]←a[i]+xa[i] \gets a[i] + x;
  2. 2 l r:求区间和 ∑i=lra[i]\sum_{i=l}^r a[i]。

正解

思路

1. 差分与前缀和的转化

朴素遍历维护区间修改与求和的时间复杂度为 O(n)O(n),在 n,q≤106n, q \le 10^6 的数据规模下会严重超时。

设差分序列为 d[i]=a[i]−a[i−1]d[i] = a[i] - a[i-1](规定 a[0]=0a[0] = 0),则由差分性质可知,原数列任意单点值为:

a[i]=∑j=1id[j]a[i] = \sum_{j=1}^i d[j]

由此,原数列的前缀和 Sk=∑i=1ka[i]S_k = \sum_{i=1}^k a[i] 可表示为差分数组的双重求和:

Sk=∑i=1k∑j=1id[j]S_k = \sum_{i=1}^k \sum_{j=1}^i d[j]

通过统计每个 d[j]d[j] 在总和中被累加的次数,可以调换求和次序:

  • d[1]d[1] 在 a[1],a[2],…,a[k]a[1], a[2], \dots, a[k] 中均出现,共计 kk 次;
  • d[2]d[2] 在 a[2],a[3],…,a[k]a[2], a[3], \dots, a[k] 中均出现,共计 k−1k - 1 次;
  • 一般地,d[j]d[j] 在 a[j],…,a[k]a[j], \dots, a[k] 中出现,共计 k−j+1k - j + 1 次。

因此前缀和公式可化简为:

Sk=∑j=1k(k−j+1)d[j]=(k+1)∑j=1kd[j]−∑j=1kj⋅d[j]S_k = \sum_{j=1}^k (k - j + 1) d[j] = (k + 1) \sum_{j=1}^k d[j] - \sum_{j=1}^k j \cdot d[j]

2. 双树状数组的维护方案

观察化简后的公式:

  • ∑j=1kd[j]\sum_{j=1}^k d[j] 是差分序列 d[j]d[j] 的前缀和;
  • ∑j=1kj⋅d[j]\sum_{j=1}^k j \cdot d[j] 是加权差分序列 j⋅d[j]j \cdot d[j] 的前缀和。

建立两个树状数组:

  1. 树状数组 T1T_1:维护序列 d[i]d[i];
  2. 树状数组 T2T_2:维护序列 i⋅d[i]i \cdot d[i]。

对于两种操作:

  • 区间加 1 l r x: 差分序列上仅有两处发生变动:d[l]d[l] 增加 xx,d[r+1]d[r+1] 减少 xx。 对应加权序列中:l⋅d[l]l \cdot d[l] 增加 l⋅xl \cdot x,(r+1)⋅d[r+1](r+1) \cdot d[r+1] 减少 (r+1)⋅x(r+1) \cdot x。 因此在 T1T_1 的位置 ll 加上 xx、位置 r+1r+1 加上 −x-x;在 T2T_2 的位置 ll 加上 l⋅xl \cdot x、位置 r+1r+1 加上 −(r+1)⋅x-(r+1) \cdot x。
  • 区间查询 2 l r: 利用前缀和相减:∑i=lra[i]=Sr−Sl−1\sum_{i=l}^r a[i] = S_r - S_{l-1}。 前缀和由公式计算:
    Sk=(k+1)⋅query⁡(T1,k)−query⁡(T2,k)S_k = (k + 1) \cdot \operatorname{query}(T_1, k) - \operatorname{query}(T_2, k)

单次修改与查询均只需对树状数组进行常数次操作,单次时间复杂度降至 O(log⁡n)O(\log n)。

代码

Python 版:

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-07-05 21:47
# update_at: 2026-07-05 21:47

import sys


def add(tree1: list[int], tree2: list[int], n: int, pos: int, val: int) -> None:
    """在位置 pos 累加差分增量 val,同时维护 d[i] 与 i*d[i] 的树状数组。"""
    val_weighted = pos * val
    while pos <= n:
        tree1[pos] += val
        tree2[pos] += val_weighted
        pos += pos & -pos


def prefix_sum(tree1: list[int], tree2: list[int], pos: int) -> int:
    """查询前缀 1..pos 的元素和:(pos + 1) * sum(d[i]) - sum(i * d[i])。"""
    sum_d = 0
    sum_id = 0
    i = pos
    while i > 0:
        sum_d += tree1[i]
        sum_id += tree2[i]
        i -= i & -i
    return (pos + 1) * sum_d - sum_id


def solve() -> None:
    data = iter(sys.stdin.buffer.read().split())
    try:
        n_token = next(data)
    except StopIteration:
        return
    n = int(n_token)
    q = int(next(data))

    tree1 = [0] * (n + 1)  # 维护差分序列 d[i]
    tree2 = [0] * (n + 1)  # 维护加权差分序列 i * d[i]

    prev = 0
    for i in range(1, n + 1):
        curr = int(next(data))
        diff = curr - prev
        add(tree1, tree2, n, i, diff)
        prev = curr

    out: list[str] = []
    for _ in range(q):
        op = next(data)
        if op == b'1':
            l = int(next(data))
            r = int(next(data))
            x = int(next(data))
            add(tree1, tree2, n, l, x)
            add(tree1, tree2, n, r + 1, -x)
        else:
            l = int(next(data))
            r = int(next(data))
            ans = prefix_sum(tree1, tree2, r) - prefix_sum(tree1, tree2, l - 1)
            out.append(str(ans))

    sys.stdout.write('\n'.join(out) + '\n')


if __name__ == '__main__':
    solve()

C++ 版(同一算法):

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-05 07:28
 * update_at: 2026-10-05 07:28
 */

// 区间加、区间求和:双树状数组维护差分序列
// 设 d[i] = a[i] - a[i-1],则前缀和
//   S[k] = sum_{j=1..k} (k-j+1) * d[j] = (k+1) * sum(d[j]) - sum(j * d[j])
// 用 t1 维护 d[i],t2 维护 i*d[i],两个操作都只需 O(log n)。

#include <cstdio>

typedef long long ll;

const int MAXN = 1000005;

int n, q;
ll t1[MAXN]; // 维护差分序列 d[i] 的树状数组
ll t2[MAXN]; // 维护加权差分序列 i*d[i] 的树状数组

// 在位置 pos 累加差分增量 val,同时维护 d 与 i*d 两个树状数组
void update(int pos, ll val) {
    if (pos > n) // r+1 可能越界,直接丢弃
        return;
    ll weighted = (ll)pos * val; // 该位置上加权序列的增量
    for (int i = pos; i <= n; i += i & (-i)) {
        t1[i] += val;
        t2[i] += weighted;
    }
}

// 查询前缀 1..pos 的元素和:(pos+1) * sum(d) - sum(i*d)
ll query(int pos) {
    ll sum_d = 0, sum_id = 0;
    for (int i = pos; i > 0; i -= i & (-i)) {
        sum_d += t1[i];
        sum_id += t2[i];
    }
    return (ll)(pos + 1) * sum_d - sum_id;
}

int main() {
    scanf("%d %d", &n, &q);
    // 初始数列按差分拆开建树:a[i]-a[i-1] 加在位置 i
    ll prev = 0;
    for (int i = 1; i <= n; ++i) {
        ll cur;
        scanf("%lld", &cur);
        update(i, cur - prev);
        prev = cur;
    }
    for (int i = 1; i <= q; ++i) {
        int op;
        scanf("%d", &op);
        if (op == 1) { // 区间加:差分上只有 d[l] 与 d[r+1] 两处变化
            int l, r;
            ll x;
            scanf("%d %d %lld", &l, &r, &x);
            update(l, x);
            update(r + 1, -x);
        } else { // 区间求和:两次前缀和相减
            int l, r;
            scanf("%d %d", &l, &r);
            printf("%lld\n", query(r) - query(l - 1));
        }
    }
    return 0;
}

复杂度

  • 时间复杂度:初始建树耗时 O(nlog⁡n)O(n \log n);每次区间加在两点处修改树状数组,耗时 O(log⁡n)O(\log n);每次区间查询执行两次前缀和查询,耗时 O(log⁡n)O(\log n)。总时间复杂度为 O((n+q)log⁡n)O((n + q) \log n)。
  • 空间复杂度:维护两个长度为 n+2n + 2 的树状数组,辅助空间复杂度为 O(n)O(n)。

图示解析

设序列长度为 kk,前缀和 SkS_k 是下方所有格子中差分项的和:

text
a[1] = d[1]
a[2] = d[1] + d[2]
a[3] = d[1] + d[2] + d[3]
...
a[k] = d[1] + d[2] + d[3] + ... + d[k]

将每一列的项数统计为一个长方形补全视角:

差分项 出现次数 矩形补全视角 (k+1)−j(k+1) - j 贡献总和
d[1]d[1] kk (k+1)−1(k + 1) - 1 (k+1)d[1]−1⋅d[1](k + 1) d[1] - 1 \cdot d[1]
d[2]d[2] k−1k - 1 (k+1)−2(k + 1) - 2 (k+1)d[2]−2⋅d[2](k + 1) d[2] - 2 \cdot d[2]
d[j]d[j] k−j+1k - j + 1 (k+1)−j(k + 1) - j (k+1)d[j]−j⋅d[j](k + 1) d[j] - j \cdot d[j]
d[k]d[k] 11 (k+1)−k(k + 1) - k (k+1)d[k]−k⋅d[k](k + 1) d[k] - k \cdot d[k]

求和即得 Sk=(k+1)∑j=1kd[j]−∑j=1kj⋅d[j]S_k = (k + 1) \sum_{j=1}^k d[j] - \sum_{j=1}^k j \cdot d[j],二重求和巧妙分解为两个独立前缀和。

总结

区间修改与区间查询通常需要带懒标记的线段树维护,代码量较大。本题通过数学推导,将差分数组的二阶求和转换为“差分前缀和”与“加权差分前缀和”的线性组合,使得两个轻量级的树状数组即可完美替代线段树,既节省了常数开销与代码量,又达到了 O((n+q)log⁡n)O((n + q) \log n) 的最优渐近复杂度。