双树状数组:区间修改与区间查询

用两个树状数组维护差分及其下标加权值,支持区间加与动态区间和。

“学习路线”

单点修改、区间查询区间修改、单点查询区间修改、区间查询(本篇)

前置回顾

上一篇已经得到:

di=aiai1,ax=i=1xdi d_i=a_i-a_{i-1},\qquad a_x=\sum_{i=1}^{x}d_i

区间 [l,r][l,r] 增加 vv,等价于:

dl+=v,dr+1=v d_l\mathrel{+}=v,\qquad d_{r+1}\mathrel{-}=v

一棵树状数组足以恢复一个位置的值。本篇的新问题是:怎样恢复一整个前缀的和。

一句话算法

一棵树状数组记录差分,另一棵记录“下标乘差分”,两者组合还原原数组前缀和。

问题模型

给定长度为 nn 的数组 aa,支持:

  1. 区间修改:给 al,al+1,,ara_l,a_{l+1},\ldots,a_r 都加上 value
  2. 区间查询:求当前 al+al+1++ara_l+a_{l+1}+\cdots+a_r

差分树状数组能查询单个 axa_x。如果为了区间和逐个查询 al,,ara_l,\ldots,a_r,一次查询最坏仍是 O(nlogn)O(n\log n)

我们需要直接计算原数组的前缀和。

核心转化

如何使用差分数组dd来求原数组的区间和aa

设原数组前缀和为:

Pre_sum(x)=i=1xai Pre\_sum(x)=\sum_{i=1}^{x}a_i

ai=j=1idja_i=\sum_{j=1}^{i}d_j 代入:

Pre_sum(x)=i=1xai=i=1xj=1idj \begin{aligned} Pre\_sum(x) &=\sum_{i=1}^{x}a_i =\sum_{i=1}^{x}\sum_{j=1}^{i}d_j \end{aligned}

这一步写成展开形式更容易看清。对 i=1i=1xx 逐行写出 aia_i

i=1:  a1=d1i=2:  a2=d1+d2i=3:  a3=d1+d2+d3    i=x:  ax=d1+d2+d3++dx \begin{aligned} i=1&:\; a_1 = d_1 \\ i=2&:\; a_2 = d_1 + d_2 \\ i=3&:\; a_3 = d_1 + d_2 + d_3 \\ &\;\;\vdots \\ i=x&:\; a_x = d_1 + d_2 + d_3 + \cdots + d_x \end{aligned}

把上面所有行加起来,就是 Pre_sum(x)Pre\_sum(x)

每一纵列分别是 d1,  d2,  ,  dxd_1,\;d_2,\;\ldots,\;d_x按列统计

  • d1d_1 从第 11 行出现到第 xx 行,共 xx 次;
  • d2d_2 从第 22 行出现到第 xx 行,共 x1x-1 次;
  • 一般地,djd_j 从第 jj 行出现到第 xx 行,共 xj+1x - j + 1 次。

于是换序之后:

Pre_sum(x)=j=1x(xj+1)dj Pre\_sum(x) = \sum_{j=1}^{x} (x-j+1)\,d_j

再把系数拆开:

(xj+1)dj=(x+1)djjdj (x-j+1)\,d_j = (x+1)\,d_j - j\cdot d_j

代入并分离求和号:

Pre_sum(x)=j=1x[(x+1)djjdj]=(x+1)j=1xdj    j=1xjdj \begin{aligned} Pre\_sum(x) &= \sum_{j=1}^{x}\bigl[(x+1)d_j - j\cdot d_j\bigr] \\[4pt] &= (x+1)\sum_{j=1}^{x} d_j \;-\; \sum_{j=1}^{x} j\cdot d_j \end{aligned}

这个式子只需要两种动态前缀和:

  1. bit_diff 维护 djd_j
  2. bit_weighted 维护 jdjj\cdot d_j

于是:

cpp
        
1
2
prefix_sum(x) = (x + 1) * sum(bit_diff, x) - sum(bit_weighted, x);

算法步骤

同步修改两棵树

对一个差分位置 pp 增加 vv 时:

cpp
        
1
2
add(bit_diff, p, v); add(bit_weighted, p, p * v);

因此,对原数组区间 [l,r][l,r] 增加 vv

cpp
        
1
2
3
4
5
add(bit_diff, l, v); add(bit_diff, r + 1, -v); add(bit_weighted, l, l * v); add(bit_weighted, r + 1, -(r + 1) * v);

查询原数组前缀和

分别查询两棵树,再代入推导出的公式:

P(x)=(x+1)sumd(x)sumid(x) P(x)=(x+1)\operatorname{sum}_d(x)-\operatorname{sum}_{id}(x)

查询任意区间和

最后仍然使用两个前缀相减:

sum(l,r)=P(r)P(l1) \operatorname{sum}(l,r)=P(r)-P(l-1)

小例子

原数组为 [1,2,3,4,5][1,2,3,4,5],给 [2,4][2,4] 增加 1010 后,差分变成:

d = [1, 11, 1, 1, -9]

查询 P(3)P(3)

j=13dj=13,j=13jdj=1+22+3=26 \sum_{j=1}^{3}d_j=13, \qquad \sum_{j=1}^{3}j d_j=1+22+3=26
P(3)=4×1326=26 P(3)=4\times13-26=26

这与修改后的前三项 1+12+13=261+12+13=26 一致。

算法证明

核心不变量bit_diff 维护当前差分 did_ibit_weighted 维护当前加权差分 idii\cdot d_i

  1. 一次区间加只改变 dld_ldr+1d_{r+1};代码在两棵树中同步记录这两个变化,所以不变量保持成立。
  2. 两棵树分别正确返回
    i=1xdii=1xidi \sum_{i=1}^{x}d_i \quad\text{和}\quad \sum_{i=1}^{x}i\cdot d_i
  3. 将这两个结果代入恒等式
    P(x)=(x+1)i=1xdii=1xidi P(x)=(x+1)\sum_{i=1}^{x}d_i-\sum_{i=1}^{x}i\cdot d_i
    得到正确的原数组前缀和。
  4. 两个正确前缀相减,得到正确的区间和。

复杂度分析

  • 初始化:逐点加入时为 O(nlogn)O(n\log n)
  • 区间修改:常数次树状数组修改,O(logn)O(\log n)
  • 区间查询:常数次树状数组查询,O(logn)O(\log n)
  • 空间复杂度:两棵长度为 nn 的树状数组,仍为 O(n)O(n)

代码模板

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
#include <bits/stdc++.h> using namespace std; // 双树状数组:区间加、区间和。 // bit_diff 维护 b[i],bit_weighted 维护 i * b[i]。 template <typename T> struct RangeFenwick { int n = 0; vector<T> bit_diff, bit_weighted; RangeFenwick(int n = 0) { init(n); } void init(int size) { n = size; bit_diff.assign(n + 1, 0); bit_weighted.assign(n + 1, 0); } static int lowbit(int x) { return x & -x; } void add(vector<T> &bit, int pos, T value) { for (int i = pos; i <= n; i += lowbit(i)) { bit[i] += value; } } T sum(const vector<T> &bit, int pos) const { T answer = 0; for (int i = pos; i > 0; i -= lowbit(i)) { answer += bit[i]; } return answer; } void range_add(int left, int right, T value) { add(bit_diff, left, value); add(bit_diff, right + 1, -value); add(bit_weighted, left, value * static_cast<T>(left)); add(bit_weighted, right + 1, -value * static_cast<T>(right + 1)); } T prefix_sum(int pos) const { return static_cast<T>(pos + 1) * sum(bit_diff, pos) - sum(bit_weighted, pos); } T range_sum(int left, int right) const { return prefix_sum(right) - prefix_sum(left - 1); } };

完整代码

输入格式与 Luogu P3372 一致:

  • 1 l r k:给区间 [l,r][l,r] 增加 kk
  • 2 l r:查询区间 [l,r][l,r] 的和。
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
75
76
77
#include <bits/stdc++.h> using namespace std; template <typename T> struct RangeFenwick { int n = 0; vector<T> bit_diff, bit_weighted; RangeFenwick(int n = 0) { init(n); } void init(int size) { n = size; bit_diff.assign(n + 1, 0); bit_weighted.assign(n + 1, 0); } static int lowbit(int x) { return x & -x; } void add(vector<T> &bit, int pos, T value) { for (int i = pos; i <= n; i += lowbit(i)) { bit[i] += value; } } T sum(const vector<T> &bit, int pos) const { T answer = 0; for (int i = pos; i > 0; i -= lowbit(i)) { answer += bit[i]; } return answer; } void range_add(int left, int right, T value) { add(bit_diff, left, value); add(bit_diff, right + 1, -value); add(bit_weighted, left, value * static_cast<T>(left)); add(bit_weighted, right + 1, -value * static_cast<T>(right + 1)); } T prefix_sum(int pos) const { return static_cast<T>(pos + 1) * sum(bit_diff, pos) - sum(bit_weighted, pos); } T range_sum(int left, int right) const { return prefix_sum(right) - prefix_sum(left - 1); } }; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; RangeFenwick<long long> bit(n); for (int i = 1; i <= n; ++i) { long long value; cin >> value; bit.range_add(i, i, value); } while (m--) { int operation, left, right; cin >> operation >> left >> right; if (operation == 1) { long long value; cin >> value; bit.range_add(left, right, value); } else { cout << bit.range_sum(left, right) << '\n'; } } return 0; }

测试用例

输入:

5 4
1 2 3 4 5
2 1 5
1 2 4 10
2 2 4
2 4 5

输出:

15
39
19

易错点

  1. 必须明确第二棵树维护的是 idii\cdot d_i,因此公式对应 (x + 1);不要与维护 (i1)di(i-1)d_i 的另一种写法混用。
  2. 修改右边界时,下标和权值都要使用 r + 1
  3. 两棵树必须同步修改;漏改任何一棵都会破坏前缀和公式。
  4. 乘法 index * value 容易超过 int,应使用 long long
  5. 这个方法依赖“区间加”和“区间和”的线性结构,不能直接处理区间赋值、区间乘法或一般区间最值。

主练习与下一步

luogu-P3372 题解

P3372 的操作模型正是区间加与区间和。题目名称虽然写着“线段树”,但在只有加法和求和时,双树状数组同样适用;题解中给出了两种做法。

求和主线到这里结束。选学分支:树状数组维护前缀最值