树状数组与差分:区间修改与单点查询
代码模板用树状数组动态维护差分数组,把区间加法变成两个边界的单点修改。
前置回顾
本篇只组合两个已经学过的结论:
不再重复推导 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:
C++
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);
}
};
Python
python
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
# 区间加(维护差分数组)、单点查询。
# 下标必须从 1 开始:lowbit(0) = 0,会让修改循环原地打转。
# Python 的 int 是任意精度,不必像 C++ 那样担心 long long 溢出。
# 调用示例:fa = RangeAddPointQueryFenwick(5); fa.range_add(1, 3, 2); fa.point_query(3)
class RangeAddPointQueryFenwick:
"""树状数组上维护差分:tree[i] 维护 diff[i - lowbit(i) + 1 .. i] 的元素和。
区间加等价于差分数组上两次单点加,单点查询等价于差分前缀和。
"""
n: int
tree: list[int]
def __init__(self, size: int = 0) -> None:
self.init(size)
def init(self, size: int) -> None:
"""重置为长度 size 的空数组,方便同一个对象换一组数据复用。"""
self.n = size
self.tree = [0] * (size + 1)
@staticmethod
def lowbit(x: int) -> int:
"""块长:x 二进制最低位那个 1 代表的值,也就是 x & -x。"""
return x & -x
def add(self, pos: int, value: int) -> None:
"""给差分数组的一个位置增加 value:diff[pos] += value。"""
i = pos
while i <= self.n:
self.tree[i] += value
i += self.lowbit(i)
def prefix_sum(self, pos: int) -> int:
"""差分前缀和:diff[1] + ... + diff[pos],恰好等于 a[pos] 的当前值。"""
answer = 0
i = pos
while i > 0:
answer += self.tree[i]
i -= self.lowbit(i)
return answer
def range_add(self, left: int, right: int, value: int) -> None:
"""区间加:a[left..right] 都加上 value。
等价于 diff[left] += value 且 diff[right + 1] -= value;
right + 1 越过末尾时无需修改(C++ 同样有这个 if 保护)。
"""
self.add(left, value)
if right + 1 <= self.n:
self.add(right + 1, -value)
def point_query(self, pos: int) -> int:
"""单点查询:返回 a[pos] 的当前值。"""
return self.prefix_sum(pos)
完整代码
输入格式与 Luogu P3368 一致:
1 l r k:给区间增加 ; 2 x:查询当前的。
C++
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;
}
Python
python
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
75
76
77
78
79
# 洛谷 P3368 树状数组 2:区间加 + 单点查。
# 树状数组维护差分数组 d,point_query(x) = sum(d[1..x]) 才是原数组 a[x]。
# 下标从 1 开始,lowbit(0) = 0 会让循环原地打转。
# C++ 用 long long 防溢出;Python int 是任意精度,不必特判。
import sys
class RangeAddPointQueryFenwick:
"""tree[i] 维护差分数组 d[i - lowbit(i) + 1 .. i] 的元素和。"""
def __init__(self, size: int = 0) -> None:
self.n = size
self.tree = [0] * (size + 1)
@staticmethod
def lowbit(x: int) -> int:
# 最低位那个 1 代表的块长,即 x & -x。
return x & -x
def add(self, pos: int, value: int) -> None:
i = pos
while i <= self.n:
self.tree[i] += value
i += self.lowbit(i)
def prefix_sum(self, pos: int) -> int:
answer = 0
i = pos
while i > 0:
answer += self.tree[i]
i -= self.lowbit(i)
return answer
def range_add(self, left: int, right: int, value: int) -> None:
self.add(left, value)
# 只有 right < n 时 right + 1 才是合法差分下标;等于 n + 1 时 add 循环不进入,可省略。
if right < self.n:
self.add(right + 1, -value)
def point_query(self, pos: int) -> int:
return self.prefix_sum(pos)
def main() -> None:
data = sys.stdin.buffer.read().split()
if not data:
return
it = iter(map(int, data))
n = next(it)
m = next(it)
bit = RangeAddPointQueryFenwick(n)
previous = 0
for i in range(1, n + 1):
value = next(it)
# 初始化喂的是差分 a[i] - a[i-1],不是 a[i] 本身。
bit.add(i, value - previous)
previous = value
out: list[str] = []
for _ in range(m):
operation = next(it)
if operation == 1:
left = next(it)
right = next(it)
value = next(it)
bit.range_add(left, right, value)
else:
pos = next(it)
out.append(str(bit.point_query(pos)))
if out:
sys.stdout.write("\n".join(out) + "\n")
if __name__ == "__main__":
main()
测试用例
输入:
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 题解
这道题直接检验“区间加转成两个差分点修改,单点值转成差分前缀和”。
下一篇:双树状数组:区间修改与区间查询。