一、简介
树状数组用于两种操作:
- 快速求前缀和 O ( l o g n ) O(logn) O(logn)
- 修改某一个数 O ( l o g n ) O(logn) O(logn)
这两个操作也可以用其他方法结构完成:
用一个数组存每个数:操作1. O ( n ) O(n) O(n),遍历前n个数求和;操作2. O ( 1 ) O(1) O(1),直接修改即可
维护前缀和一个数组:操作1. O ( 1 ) O(1) O(1);操作2. O ( n ) O(n) O(n)
可见其他结构最坏情况都是 O ( n ) O(n) O(n)
二、算法
已知一个数可以拆成若干个2的幂次之和
x = 2 i 1 + 2 i 2 + 2 i 3 + . . . + 2 i k x=2^{i_1}+2^{i_2}+2^{i_3}+...+2^{i_k} x=2i1+2i2+2i3+...+2ik
其中 i 1 ≤ i 2 ≤ . . . ≤ i k i_1 \leq i_2 \leq ... \leq i_k i1≤i2≤...≤ik
所以区间 ( 0 , x ] (0, x] (0,x]可以拆成
( x − 2 i 1 , x ] (x-2^{i_1},x] (x−2i1,x]
( x − 2 i 1 − 2 i 2 , x − 2 i 1 ] (x-2^{i_1}-2^{i_2},x-2^{i_1}] (x−2i1−2i2,x−2i1]
.
.
.
( x − 2 i 1 − 2 i 2 − . . . − 2 i k , x − 2 i 1 − 2 i 2 − . . . − 2 i k − 1 ] (x-2^{i_1}-2^{i_2}-...-2^{i_k},x-2^{i_1}-2^{i_2}-...-2^{i_{k-1}}] (x−2i1−2i2−...−2ik,x−2i1−2i2−...−2ik−1]
也就是
( 0 , x − 2 i 1 − 2 i 2 − . . . − 2 i k − 1 ] (0,x-2^{i_1}-2^{i_2}-...-2^{i_{k-1}}] (0,x−2i1−2i2−...−2ik−1]
再来看这些区间包含多少个数
( x − 2 i 1 , x ] (x-2^{i_1},x] (x−2i1,x]包含 2 i 1 2^{i_1} 2i1个数,其中 i 1 i_1 i1是 x x x二进制表示的最后一位1的位置
( x − 2 i 1 − 2 i 2 , x − 2 i 1 ] (x-2^{i_1}-2^{i_2},x-2^{i_1}] (x−2i1−2i2,x−2i1]包含 2 i 2 2^{i_2} 2i2个数,其中 i 2 i_2 i2是 x − 2 i 1 x-2^{i_1} x−2i1二进制表示的最后一位1的位置
( 0 , x − 2 i 1 − 2 i 2 − . . . − 2 i k − 1 ] (0,x-2^{i_1}-2^{i_2}-...-2^{i_{k-1}}] (0,x−2i1−2i2−...−2ik−1]包含 2 i k 2^{i_k} 2ik个数,其中 i k i_k ik是 x − 2 i 1 − 2 i 2 − . . . − 2 i k − 1 x-2^{i_1}-2^{i_2}-...-2^{i_{k-1}} x−2i1−2i2−...−2ik−1二进制表示的最后一位1的位置。
性质1:对于区间 ( L , R ] (L,R] (L,R],区间包含的数的个数是R的二进制表示的最后一位1的位置对应次幂。
这个最后一位1的位置对应次幂可以用lowbit(x) = x & (-x)
计算求得,因为计算机里的负数就是对原数的二进制取反+1
例如: 6 = 11 0 2 6=110_2 6=1102, − 6 = 01 0 2 -6=010_2 −6=0102,lowbit(6) = 110 & 010 = 10
,即6最后一位1对应次幂是 1 0 2 = 2 1 10_2=2^1 102=21
再来看如何拆分区间: x = 6 = 2 2 + 2 1 x=6=2^2+2^1 x=6=22+21,二进制是 11 0 2 110_2 1102,因此 i 1 = 1 i_1=1 i1=1, i 2 = 2 i_2=2 i2=2
所以区间 ( 0 , 6 ] (0, 6] (0,6]可以拆成:
( 6 − 2 1 , 6 ] = ( 4 , 6 ] (6-2^1,6]=(4,6] (6−21,6]=(4,6],其中 i 1 = 1 i_1=1 i1=1是 6 6 6二进制表示 110 110 110最后一位1的位置
( 6 − 2 1 − 2 2 , x − 2 2 ] = ( 0 , 4 ] (6-2^1-2^2,x-2^2]=(0,4] (6−21−22,x−22]=(0,4],其中 i 2 = 2 i_2=2 i2=2是 4 4 4二进制表示 100 100 100最后一位1的位置
有了性质1,可以得到 L = R − l o b i t ( R ) L=R-lobit(R) L=R−lobit(R),故区间为 ( R − l o b i t ( R ) , R ] (R-lobit(R),R] (R−lobit(R),R]
用数组 t [ x ] t[x] t[x]表示区间 [ x − l o b i t ( x ) + 1 , x ] [x-lobit(x)+1, x] [x−lobit(x)+1,x]的和,则 t [ 0 ] t[0] t[0]~ t [ 8 ] t[8] t[8]如下所示
可以看到形成一个类似树的结构,例如t[8]的孩子就有 t [ 4 ] , t [ 6 ] , t [ 7 ] t[4], t[6], t[7] t[4],t[6],t[7]。
这个树有如下性质:
性质2:
通过父节点 t [ x ] t[x] t[x]找子节点:令 x ′ = x − 1 x'=x-1 x′=x−1,之后不断去掉 x ′ x' x′的最后一位1即可。
即对于 x = . . . 10...00 0 2 x=...10...000_2 x=...10...0002,得到 x ′ = x − 1 = . . . 01...11 1 2 x'=x-1=...01...111_2 x′=x−1=...01...1112,所以有孩子 . . . 01...11 1 2 ...01...111_2 ...01...1112、 . . . 01...11 0 2 ...01...110_2 ...01...1102、 . . . 01...10 0 2 ...01...100_2 ...01...1002、 . . . 01...00 0 2 ...01...000_2 ...01...0002、…、 . . . 00...00 0 2 ...00...000_2 ...00...0002
例如 t [ 8 ] t[8] t[8],得到 x ′ = 100 0 2 − 1 2 = 11 1 2 x'=1000_2-1_2=111_2 x′=10002−12=1112,则孩子有 t [ 11 1 2 ] , t [ 11 0 2 ] , t [ 10 0 2 ] t[111_2],t[110_2],t[100_2] t[1112],t[1102],t[1002]也就是 t [ 7 ] , t [ 6 ] , t [ 4 ] t[7],t[6],t[4] t[7],t[6],t[4]
通过性质2,可以由孩子求出父亲,例如 t [ 8 ] = a [ 8 ] + t [ 4 ] + t [ 6 ] + t [ 7 ] t[8]=a[8]+t[4]+t[6]+t[7] t[8]=a[8]+t[4]+t[6]+t[7]
查询a[1] + ... + a[x]
-> for(int i = x; i > 0; i -= lobit(i)) sum += t[i]
性质3:
通过子节点找父节点:将父节点找子节点过程逆过来即可
对于孩子 x = . . . 01...10 0 2 x=...01...100_2 x=...01...1002,父亲一定是 . . . 10...00 0 2 ...10...000_2 ...10...0002,可以看到孩子只有唯一一个父亲
所以只需 . . . 01...10 0 2 + . . . 00...10 0 2 ...01...100_2+...00...100_2 ...01...1002+...00...1002即可得到父亲 . . . 10...000 ...10...000 ...10...000
也就是x + lobit(x)
通过性质3,可以找到更新原数组时所需要更新的对应的树状数组
原数组修改a[x] += c
-> 树状数组修改for(int i = x; i <= n; i += lobit(i)) t[i] += c
注意:树状数组维护的位置下表只可以从1开始,如果为0则的lowbit也是0,会死循
三、例题
树状数组模板
注意
#include <iostream>
using namespace std;const int N = 500005;
int n, m;
int t[N], a[N];int lowbit(int x) {return x & (-x);
}int sum(int x) {int s = 0;for (int i = x; i > 0; i -= lowbit(i)) s += t[i];return s;
}void add(int x, int c) {for (int i = x; i <= n; i += lowbit(i)) {t[i] += c;}
}int main() {scanf("%d%d", &n, &m);for (int i = 1; i <= n; i ++ ) {scanf("%d", &a[i]);add(i, a[i]);}int op, x, y;for (int i = 1; i <= m; i ++ ) {scanf("%d%d%d", &op, &x, &y);if (op == 1) {add(x, y);}else {printf("%d\n", sum(y) - sum(x - 1));}}return 0;
}