树状数组与差分:区间修改与单点查询
用树状数组动态维护差分数组,把区间加法变成两个边界的单点修改。
前置回顾
本篇只组合两个已经学过的结论:
不再重复推导 lowbit 和普通差分,只关注两者怎样组合成在线算法。
一句话算法
把区间加法记在差分数组的两个边界上,再用树状数组求差分前缀和恢复单点值。
问题模型
给定长度为
- 区间修改:给
都加上 value; - 单点查询:询问当前的
。
如果每次区间修改都逐项更新,最坏需要
核心转化
令:
给区间

任意位置

因此,问题变成基础树状数组最擅长的模型:
原问题:区间修改 + 单点查询
|
v
差分数组:两个单点修改 + 一次前缀查询
算法步骤
初始化
读入原数组时计算相邻差:
cpp
1
2
3
4
5
6
previous = 0;
for (int i = 1; i <= n; ++i) {
cin >> value;
bit.add(i, value - previous);
previous = value;
}
树状数组实际维护的是
区间修改
对
cpp
1
2
add(l, v);
if (r < n) add(r + 1, -v);
d[l] += v 表示影响从 d[r+1] -= v 表示影响在
单点查询
查询
cpp
1
point_query(x) = prefix_sum(x);
例如原数组为
原差分: 1 1 1 1 1
边界修改: +10 -10
新差分: 1 11 1 1 -9
前缀还原:1 12 13 14 5
查询位置
算法证明
核心不变量:树状数组始终维护当前数组的差分
区间
增加 ,所以从 开始的前缀和都多出 ; 减少 ,所以从 开始抵消这次增加; - 因而只有
增加 。
树状数组正确完成两个差分点修改,并正确返回差分前缀和,所以每次单点查询得到当前真实的
复杂度分析
- 初始化:
; - 区间修改:两次单点修改,
; - 单点查询:一次前缀查询,
; - 空间复杂度:
。
代码模板
模板中的 add 操作修改一个差分位置,日常使用的接口是 range_add 和 point_query:
cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <bits/stdc++.h>
using namespace std;
// 维护差分数组:区间加、单点查询。
// 下标必须从 1 开始。
template <typename T>
struct RangeAddPointQueryFenwick {
int n = 0;
vector<T> tree;
RangeAddPointQueryFenwick(int n = 0) { init(n); }
void init(int size) {
n = size;
tree.assign(n + 1, 0);
}
static int lowbit(int x) { return x & -x; }
// 给差分数组的一个位置增加 value。
void add(int pos, T value) {
for (int i = pos; i <= n; i += lowbit(i)) {
tree[i] += value;
}
}
T prefix_sum(int pos) const {
T answer = 0;
for (int i = pos; i > 0; i -= lowbit(i)) {
answer += tree[i];
}
return answer;
}
void range_add(int left, int right, T value) {
add(left, value);
if (right + 1 <= n) add(right + 1, -value);
}
T point_query(int pos) const {
return prefix_sum(pos);
}
};
完整代码
输入格式与 Luogu P3368 一致:
1 l r k:给区间增加 ; 2 x:查询当前的。
cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <bits/stdc++.h>
using namespace std;
template <typename T>
struct RangeAddPointQueryFenwick {
int n = 0;
vector<T> tree;
RangeAddPointQueryFenwick(int n = 0) { init(n); }
void init(int size) {
n = size;
tree.assign(n + 1, 0);
}
static int lowbit(int x) { return x & -x; }
void add(int pos, T value) {
for (int i = pos; i <= n; i += lowbit(i)) {
tree[i] += value;
}
}
T prefix_sum(int pos) const {
T answer = 0;
for (int i = pos; i > 0; i -= lowbit(i)) {
answer += tree[i];
}
return answer;
}
void range_add(int left, int right, T value) {
add(left, value);
if (right < n) add(right + 1, -value);
}
T point_query(int pos) const {
return prefix_sum(pos);
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
RangeAddPointQueryFenwick<long long> bit(n);
long long previous = 0;
for (int i = 1; i <= n; ++i) {
long long value;
cin >> value;
bit.add(i, value - previous);
previous = value;
}
while (m--) {
int operation;
cin >> operation;
if (operation == 1) {
int left, right;
long long value;
cin >> left >> right >> value;
bit.range_add(left, right, value);
} else {
int pos;
cin >> pos;
cout << bit.point_query(pos) << '\n';
}
}
return 0;
}
测试用例
输入:
5 4
1 2 3 4 5
2 3
1 2 4 10
2 3
2 5
输出:
3
13
5
位置
易错点
- 树状数组维护的是差分数组,
point_query(x)才是原数组的。 - 只有当
r < n时,r+1才是有效差分下标。 - 初始化时要加入
a[i] - a[i-1],不能直接加入a[i]。 - 区间修改的右边界必须写成
-value,用于停止影响。
主练习与下一步
luogu-P3368 题解
这道题直接检验“区间加转成两个差分点修改,单点值转成差分前缀和”。
下一篇:双树状数组:区间修改与区间查询。