区间加在差分数组上只改两个位置;再展开一层前缀和得 S(x)=(x+1)Σd_j-Σj·d_j,用两棵树状数组分别维护 d_j 与 j·d_j,区间加与区间求和都是 O(log N)。
OJ: roj
题目 ID: 3110
难度:提高
标签:树状数组差分前缀和python
创建: 2026-10-01 17:40
更新: 2026-10-10 11:30
形式化题目
给定长度为
C l r d:令每个数都加上 ; Q:输出某个区间当前的元素之和。
所有询问按输入顺序逐行输出答案。约束为
题面文字与评测数据的一处不一致
题面原文把询问写作 Q X(询问第 Q l r:
十组 data/*.in 里 Q 行一律带两个操作数,对应的 data/*.out 也是区间和,
且大数据的答案达到
以题面样例为例(此样例用的是 Q X 单点版本):
10 5
1 2 3 4 5 6 7 8 9 10
Q 4
Q 1
Q 2
C 1 6 3
Q 2样例输出为 4 1 2 5:前三条 Q 时还没发生过任何修改,所以答案就是原数列的
C 1 6 3 把前三项之外的前 Q 2 读到的是
正解
思路
1. 朴素做法与它的瓶颈
最直接的写法是维护原数组
C l r d:for i in range(l, r+1): A[i] += d,; Q X:直接读A[X],。
这个写法的问题是一次区间加要逐个改到区间里的每个元素。
还有另一种朴素写法:维护差分数组
| 写法 | 区间加 | 单点求值 |
|---|---|---|
| 维护 |
||
| 维护差分 |
两种写法各留下一个
2. 关键观察:区间加在差分上只动两个位置
对
时, 与 同时加 ,故 不变; - 只有跨越区间边界的两个位置变化:
, 。
而单点求值恰好是差分的前缀和:
于是「区间加 + 单点求值」被翻译成「单点改 + 前缀和」,
这正是树状数组最擅长的操作组合,两者都是
下面用题面样例把这条观察走一遍。表中加粗的是本次被改动的位置:
| 指令 | 含义 | 差分数组变化 | 操作后的 |
输出 |
|---|---|---|---|---|
| (初始) | 由 |
1 · 1 · 1 · 1 · 1 · 1 · 1 · 1 · 1 · 1 | ||
Q 4 |
询问 |
无(只读) | 1 · 1 · 1 · 1 · 1 · 1 · 1 · 1 · 1 · 1 | |
C 1 6 3 |
4 · 1 · 1 · 1 · 1 · 1 · -2 · 1 · 1 · 1 | |||
Q 2 |
询问 |
无(只读) | 4 · 1 · 1 · 1 · 1 · 1 · -2 · 1 · 1 · 1 |
观察 C 1 6 3 那一行:Q 行与它们的输出:C 1 6 3 之后
3. 树状数组维护「单点改 + 前缀和」
树状数组的结点
- 单点加:沿
上溯,更新所有覆盖该位置的结点; - 前缀和:沿
下跳,累加沿途结点。
下表以
| 结点 |
覆盖区间 |
本次角色 | |
|---|---|---|---|
| 1 | 1 | — | |
| 2 | 2 | — | |
| 3 | 1 | — | |
| 4 | 4 | 查 | |
| 5 | 1 | 改 | |
| 6 | 2 | 改 查 | |
| 7 | 1 | 查 | |
| 8 | 8 | 改 |
先看覆盖区间一列:结点管辖的长度按
4. 评测数据要区间和:再展开一层前缀和
数据实际要求
(等号后的直观解释:每个
右边是两个前缀和,各自都能用一棵树状数组维护:
bit1维护; bit2维护。
一次区间加要同步改两棵树(共 4 次单点加):
一次区间和查询要 4 次前缀和:
5. 初始化与两个实现细节
初始化:由 bit1.add(i, d_i) 与 bit2.add(i, d_i * i),共
首次实现时一个容易忽略的边界:当 if r < n: 拦住,否则会写到序列之外并污染后续查询。
第二个细节是让单点 Q X 与区间 Q l r 共用同一条查询路径。
因为 Q 行少了一个操作数,
所以按行切分后判断操作数个数即可:right = int(parts[2]) if len(parts) > 2 else left。
代码
/**
* 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-10 11:30
* update_at: 2026-10-10 11:30
*/
// 区间加 + 区间和:两个树状数组维护差分序列 d[i] 与 i·d[i]。
#include <cstdio>
#include <string>
#include <sstream>
#include <iostream>
#include <vector>
typedef long long ll;
const ll MAXN = 100005;
ll n, m;
ll bit1[MAXN]; // 差分数组 d 的树状数组
ll bit2[MAXN]; // i·d[i] 的树状数组
// 在下标 i 处加 delta:沿 i += i & -i 上溯
void bit_add(ll* bit, ll i, ll delta) {
while (i <= n) {
bit[i] += delta;
i += i & -i;
}
}
// 求 bit 维护序列的前 i 项和
ll bit_sum(ll* bit, ll i) {
ll total = 0;
while (i > 0) {
total += bit[i];
i -= i & -i;
}
return total;
}
// 把 [l, r] 整体加 delta:差分序列上只需改 d[l] 和 d[r+1] 两个位置
void range_add(ll l, ll r, ll delta) {
bit_add(bit1, l, delta);
bit_add(bit2, l, delta * l);
if (r < n) { // r = n 时 d[n+1] 落在序列之外,不记录
bit_add(bit1, r + 1, -delta);
bit_add(bit2, r + 1, -delta * (r + 1));
}
}
// Σ_{i≤x} A[i] = (x+1)·Σ d[j] - Σ j·d[j]
ll prefix_sum(ll x) {
return (x + 1) * bit_sum(bit1, x) - bit_sum(bit2, x);
}
int main() {
std::string line;
// 第一行:n m(跳过空行)
while (std::getline(std::cin, line)) {
std::istringstream iss(line);
if (iss >> n >> m) {
break;
}
}
// 数列可能被折成多行,读满 n 个数为止
std::vector<ll> values;
while ((ll)values.size() < n && std::getline(std::cin, line)) {
std::istringstream iss(line);
ll v;
while (iss >> v) {
values.push_back(v);
}
}
ll prev = 0; // 前一个数,用来就地算差分 d[i] = A[i] - A[i-1]
for (ll i = 1; i <= n; i++) {
ll diff = values[i - 1] - prev;
prev = values[i - 1];
bit_add(bit1, i, diff);
bit_add(bit2, i, diff * i);
}
std::vector<std::string> out;
for (ll q = 0; q < m; q++) {
if (!std::getline(std::cin, line)) {
break;
}
std::istringstream iss(line);
std::string op;
ll parts[8];
ll cnt = 0;
if (!(iss >> op)) {
q--; // 空行不算一条指令
continue;
}
while (cnt < 8 && iss >> parts[cnt]) {
cnt++;
}
ll left = parts[0];
if (op == "C") {
ll right = parts[1], delta = parts[2];
range_add(left, right, delta);
} else { // 单点询问少一个操作数,取 right = left
ll right = (cnt > 1) ? parts[1] : left;
std::ostringstream oss;
oss << (prefix_sum(right) - prefix_sum(left - 1));
out.push_back(oss.str());
}
}
for (size_t i = 0; i < out.size(); i++) {
printf("%s\n", out[i].c_str());
}
return 0;
}#!/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-01 17:40
# update_at: 2026-10-01 17:54
import sys
from collections.abc import Iterator
ADD = b"C" # 区间加指令 C l r d;询问指令是 "Q",题面写 "Q X"(单点值),评测数据写 "Q l r"(区间和)
def bit_add(bit: list[int], i: int, delta: int, limit: int) -> None:
"""在下标 i 处加 delta:沿 i += i & -i 上溯,更新所有覆盖 i 的结点。"""
while i <= limit:
bit[i] += delta
i += i & -i
def bit_sum(bit: list[int], i: int) -> int:
"""求 bit 维护序列的前 i 项和:沿 i -= i & -i 下跳,把沿途结点累加。"""
total = 0
while i > 0:
total += bit[i]
i -= i & -i
return total
def range_add(bit1: list[int], bit2: list[int], n: int, l: int, r: int, delta: int) -> None:
"""把 [l, r] 整体加 delta:差分序列上只需改 d[l] 和 d[r+1] 两个位置。
bit1 维护 d[i],bit2 维护 i·d[i]。r = n 时 d[n+1] 落在序列之外,
差分本来到这里就结束了,不必也不能记录。
"""
bit_add(bit1, l, delta, n)
bit_add(bit2, l, delta * l, n)
if r < n:
bit_add(bit1, r + 1, -delta, n)
bit_add(bit2, r + 1, -delta * (r + 1), n)
def prefix_sum(bit1: list[int], bit2: list[int], x: int) -> int:
"""求 A[1..x] 的和。
交换求和次序:Σ_{i≤x} A[i] = Σ_{i≤x} Σ_{j≤i} d[j] = Σ_{j≤x} d[j]·(x-j+1)
= (x+1)·Σ d[j] - Σ j·d[j];两个和式分别由 bit1、bit2 查询得到。
"""
return (x + 1) * bit_sum(bit1, x) - bit_sum(bit2, x)
def solve() -> None:
stdin = sys.stdin.buffer
# 按行切分并滤掉空行:bytes.split 无参切分与 line.split() 等价,用 map 只需切一次。
lines: Iterator[list[bytes]] = (parts for parts in map(bytes.split, stdin) if parts)
n, m = map(int, next(lines))
bit1 = [0] * (n + 2) # 差分数组 d 的树状数组
bit2 = [0] * (n + 2) # i·d[i] 的树状数组
values: list[int] = []
while len(values) < n: # 数列可能被折成多行,读满 n 个数为止
values += map(int, next(lines))
prev = 0 # 前一个数,用来就地算差分 d[i] = A[i] - A[i-1](A[0] 视作 0)
for i, value in enumerate(values, 1):
diff = value - prev
prev = value
bit_add(bit1, i, diff, n)
bit_add(bit2, i, diff * i, n)
out: list[str] = []
for _ in range(m):
parts = next(lines)
op, left = parts[0], int(parts[1])
if op == ADD:
right, delta = int(parts[2]), int(parts[3])
range_add(bit1, bit2, n, left, right, delta)
else: # 单点询问少一个操作数,取 right = left 就退化成区间和 [x, x]
right = int(parts[2]) if len(parts) > 2 else left
out.append(str(prefix_sum(bit1, bit2, right) - prefix_sum(bit1, bit2, left - 1)))
sys.stdout.write('\n'.join(out) + '\n' if out else '')
if __name__ == "__main__":
solve()结构与上面的推导一一对应:
bit_add/bit_sum是树状数组的两个基本操作,对应第 3 步的一上溯、一下跳;range_add是第 2 步的落地,其中if r < n:处理「不改 」; prefix_sum是第 4 步的公式,(x + 1) * bit_sum(bit1, x) - bit_sum(bit2, x)一眼对应; solve里的prev记录,就地算出差分完成初始化; 指令分流处的 len(parts) > 2处理两种询问格式。
复杂度
- 时间复杂度:初始化
,单次区间加或区间和查询均为 , 总计 。 取 约 次内层循环,实测最慢用例 0.38 s (限制 1000 ms)。对比朴素做法 ,差了约 3000 倍。 - 空间复杂度:两棵长度
的树状数组,加读入的数列与答案列表,共 。 实测峰值 34 MB(限制 128 MB)。 - 数值范围:区间和最大约
,Python 整数无溢出; 若用 C++ 实现, bit1、bit2与答案都必须用long long。
总结
- 朴素做法的瓶颈是「一次区间加要逐个改元素」;而整个区间加的是同一个
, 改动本身高度局部,应当换个角度记录它。 - 关键观察一:差分把区间加压缩成
、 两个点; 同时把单点求值变成差分的前缀和。 - 关键观察二:树状数组让「单点改 + 前缀和」都变成
, 正好匹配观察一留下的操作组合。 - 区间和的推广:展开
得 , 用两棵树状数组分别维护 与 ,即「区间加 + 区间求和」的标准双 BIT 模型。 - 边界:
时不改 ; Q X是Q l r在时的退化情形。 - 复杂度
时间、 空间。
图示解析
这张图串起本题从建模到得到答案的主线:
区间加 + 单点求值 (Q X)
`- 定义差分 d_i = A_i - A_(i-1)
|- 区间加 A_l..A_r += v 等价于 d_l += v, d_(r+1) -= v (只有 2 个点)
`- 单点求值 A_x 等价于 d_1 + ... + d_x (前缀和)
`- 点修改 + 前缀和 -> 树状数组, O(log N)
评测数据实际要求区间求和 (Q l r)
`- 再展开一层: S(x) = (x+1)·Σd_j - Σj·d_j
`- 两个前缀和 -> 两棵树状数组 bit1 维护 d, bit2 维护 i·d_i
`- 答案 = S(r) - S(l-1), 单点询问是 l = r 的退化区间先看第一段分支:它把「整段加」换成「两个点加」,把「取一个数」换成「求前缀和」,
这一步消掉了朴素解法的