树状数组

什么是树状数组

有这样一个问题,给定一个数组,求它的前 nn 项和。
1
最简单粗暴的方式,就是一个个加起来,存到一个大小为 nn 的数组里面,时间复杂度为 O(n)O(n)
但是有一个数变了,由 b1b_1 变为了 b2b_2,需要更新上述的数组,那么更改的时间复杂度为 O(n)O(n),因为为了维护上述数组的性质,需要在变更的数之后的所有数都给加上这个变化 b2b1b_2 - b_1。求和的时间复杂度为 O(1)O(1),因为拿到前缀和之后数组的结果就是答案了。
如果这个数组要被频繁修改,那这种方式效率就很低下了。有没有更快的方式呢?

树状数组的雏形

我们可以做以下处理:
2
首先将每两个数的和都记录下来,那么求和的时候就可以只求 (n/2)(n/2) 次了。那么再对第二层的数组再做一个两两求和…次数就变为 (n/4)(n/4) 次了。以此类推。最后就变成上图这个样子了。如果是这样的话,修改一个数,我们只需要更改 log2nlog_2n个数就可以了!
仔细观察上述数组,我们发现有一些数字是根本用不上的。第 ii 层的偶数个数都用不上。那么他就会变成这个样子:
3
这样空间复杂度就从 O(n2)O(n^2) 退化为 O(n)O(n) 了。这其中的每个数代表一段区间内数字的和。

lowerbits

在介绍树状数组的操作之前,先引入一个函数,这个函数代码如下:

1
2
3
inline int lowerbits(int x) {
return x & (-x);
}

这个函数的作用是将一个数的二进制格式中最低位的1单独拿出来,比如:

1
2
binary(12) = 00001100   (就展示最低的一个字节)
lowerbits(12) = 00000100 (取最后一个1之后的部分)

这个函数在树状数组中有什么作用呢?lowerbits(x)lowerbits(x)指代的是下标为 xx 的树状数组元素所代表的原数组的区间长度。如图:
4
lowerbits(12)=4lowerbits(12) = 4 代表树状数组 1212 下标(从 11 开始)所代表的原数组中 44 个数字元素的和。即树状数组元素 1010 是原数组 3,1,2,43,1,2,4 的和。

用树状数组的求和

5
顺着图上的树形结构依次往左上角求和。如图所示。求前 1414 项和的值等于 b[14]+b[12]+b[8]b[14] + b[12] + b[8]
代码如下:

1
2
3
4
5
6
7
8
int count(int n) {
int res = 0;
while (n > 0) {
res += b[n];
n -= lowerbits(n);
}
return res;
}

由此可见,利用树状数组求和的时间复杂度是 O(log2n)O(log_2n)

更新树状数组

6
更新操作则是把所有包含了新数字的树状数组元素统统更新一遍,如图中红线所示。顺着树形结构向右上角更新。

1
2
3
4
5
6
7
// delta 是第 i 位数字的变化值
void add(int i, int delta) {
while (i < N) {
i += delta;
i += lowerbits(i);
}
}

由此可见,更新时间复杂度为 O(log2n)O(log_2n),比最开始的方式快了很多。

初始化树状数组

以下表示中 a[i]a[i] 为原数组元素,b[i]b[i] 为树状数组元素。

直接初始化为0,并且对每个点调用add。

1
2
3
4
5
void init() {
for (int i = 1; i < N; i++) {
add(i, a[i]);
}
}

时间复杂度 O(nlog2n)O(nlog_2n)


使用前缀和辅助数组

1
2
3
4
5
6
void init() {
for (int i = 1; i < N; i++) {
pre[i] = pre[i - 1] + a[i];
b[i] = pre[i] - pre[i - lowerbits(i)];
}
}

这种方式时间复杂度为 O(n)O(n),但是要占用额外的空间。