使用两个树状数组分别维护差分序列及其加权和,将区间加与区间查询均优化至对数复杂度。
OJ: roj
题目 ID: 1548
难度:提高
标签:树状数组差分前缀和
创建: 2026-09-30 17:30
更新: 2026-10-07 11:01
形式化题目
给定长度为
1 l r x:对所有,令 ; 2 l r:求区间和。
正解
思路
1. 差分与前缀和的转化
朴素遍历维护区间修改与求和的时间复杂度为
设差分序列为
由此,原数列的前缀和
通过统计每个
在 中均出现,共计 次; 在 中均出现,共计 次; - 一般地,
在 中出现,共计 次。
因此前缀和公式可化简为:
2. 双树状数组的维护方案
观察化简后的公式:
是差分序列 的前缀和; 是加权差分序列 的前缀和。
建立两个树状数组:
- 树状数组
:维护序列 ; - 树状数组
:维护序列 。
对于两种操作:
- 区间加
1 l r x: 差分序列上仅有两处发生变动:增加 , 减少 。 对应加权序列中: 增加 , 减少 。 因此在 的位置 加上 、位置 加上 ;在 的位置 加上 、位置 加上 。 - 区间查询
2 l r: 利用前缀和相减:。 前缀和由公式计算:
单次修改与查询均只需对树状数组进行常数次操作,单次时间复杂度降至
代码
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;
}复杂度
- 时间复杂度:初始建树耗时
;每次区间加在两点处修改树状数组,耗时 ;每次区间查询执行两次前缀和查询,耗时 。总时间复杂度为 。 - 空间复杂度:维护两个长度为
的树状数组,辅助空间复杂度为 。
图示解析
设序列长度为
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]将每一列的项数统计为一个长方形补全视角:
| 差分项 | 出现次数 | 矩形补全视角 |
贡献总和 |
|---|---|---|---|
求和即得
总结
区间修改与区间查询通常需要带懒标记的线段树维护,代码量较大。本题通过数学推导,将差分数组的二阶求和转换为“差分前缀和”与“加权差分前缀和”的线性组合,使得两个轻量级的树状数组即可完美替代线段树,既节省了常数开销与代码量,又达到了