我们在 0x01 节探讨过 lowbit 运算,并介绍了如何利用 lowbit 运算找出整数在二进制表示下所有等于 1 的位。类似地,给定一个整数 X,下面这段代码可以计算出区间 [1,X] 分成的 logX 个小区间。
while (x > 0) {
printf("[%d, %d]\n", x - (x & (-x)) + 1, x);
x -= (x & (-x));
}
树状数组 (Binary Indexed Trees) 就是一种基于上诉思想的数据结构,其基本用途是维护序列的前缀和。 对于给定的序列 A,我们建立一个数组 C,其中 C[X] 保存序列 A 的区间 [ X - lowbit(X) + 1, X] 中所有数的和。
事实上,数组C可以看作一个如下图的树形结构。每个内部节点保存以它为根的子树中所有叶节点的和。每个内节点的子节点个数等于 lowbit(X) 的位数,除树根外,每个内部节点的父节点是 t[ X + lowbit(X) ],树的深度为 logN。
如果 N 不是2的整次幂,那么树状数组就是一个具有同样性质的森林结构。
树状数组支持的基本操作有两个,第一个操作是查询前缀和,即序列 A 第 1 -> X 个数的和。按照我们刚才提出的方法,应该求出 X 的二进制表示中每个等于 1 的位,把 [1,X] 分成 logN 个小区间,而每个小区间的区间和都已经保存在数组 C 中。所有,我们可以在O(logN)的时间内查询前缀和。
int /*long long*/ ask(int x) {
int /*long long */ ans = 0;
for (; x; x -= x & -x) {
ans += c[x];
}
return ans;
}
当然,若要查询序列 A 的区间 [L,R] 中所有数的和, 只需计算 ask(R) - ask(L - 1)。
树状数组支持的第二个基本操作是单点增加,意思是给序列中的一个数 A[X] 加上 y,同时正确维护序列的前缀和。根据上面给出的树形结构和它的性质,只有节点 C[X] 极其所以祖先节点保存的 "区间和" 包含 A[X],而任意一个节点的祖先至多只有 logN 个,我们逐一对他们的 C 值进行更新即可。下面的代码在 O(logN) 时间内执行单点增加操作。
void add(int x, int y) {
for (; x <= n; x += lowbit(x)) c[x] += y