对于一个有n个元素的数组,令下标从1开始,假如给出一堆询问,Q(L, R),询问A[L] + A[L+1] + … + A[R],怎样实现?
如果直接遍历的话,每次都需要O(n)的时间,太长。优化的办法也很简单,额外开一个数组S,令S[0] = 0, S[i]表示 A[1] + … + A[i],则Q(L, R) = S[R] - S[L - 1],先用O(n)的时间预处理,之后就能在O(1)的时间得到结果。
对于数组内元素固定不再动态变化的数组,这样就可以了。然而,如果再增加一个Add(i, d)的操作,代表令A[i] += d的操作,那么进行一次Add操作就要修改之后所有的前缀和,这样每次Add操作就需要O(n)的时间,又太长了。这个时候就用到了二叉索引树了。
二叉索引树用到了一个操作,lowbit。先介绍下,lowbit(x)求的是x的二进制表达式中最右边1对应的值,注意是对应的值而不是第几位,例如,lowbit(2) = 2,lowbit(3) = 1,lowbit(8) = 8。如何求lowbit(x)呢,也好办,lowbit(x) = x & -x,因为补码等于按位取反加一,这样算出来正好是需要的结果。
接下来就是二叉索引树的结构了,它是这个样的:
圆圈就是结点,纵坐标是lowbit的值,lowbit越大越靠近根。从圆圈中间向左的黑线表示该结点保存了哪个范围的连续和,lowbit值是1的点保存的就是自己本身的,例如,8这个点保存了从1号结点到8号结点的连续和。
由这个结构不难发现,对于结点i,它右边第一个lowbit值比它大的结点(如果存在)是i + lowbit(i),它左边第一个lowbit值比它大的结点(如果存在)是i - lowbit(i)。
接下来就是怎么求和和怎么修改了。
求和的时候,例如要求前缀和S[i],那么我们只需要顺着结点i一路向左上走,然后把沿途结点的值加起来就成了。例如求S[11],路线为11→10→8,把这三个结点中保存的连续和加起来就可以了。不难发现,这些结点的连续和加起来正好是我们所求的前缀和。怎么往左上方走呢?也很简单,走到结点左边第一个lowbit值比它大的结点就行了,即减去结点自身的lowbit值。
而修改的时候,则是从结点一路向右上方走,不难发现,沿途经过的结点正好是需要更新的结点。
两个操作的时间复杂度均为O(logn),使用前预处理,清空数组然后执行n次add操作,总的复杂度是O(nlogn)。
class BIT { // Binary Indexed Tree, BIT
public:
static const int MAXN = 100000 + 10;
int C[MAXN];
public:
BIT() { memset(C, 0, sizeof(C)); }
BIT(const BIT &b) { memcpy(this, &b, sizeof(BIT)); }
private:
int lowbit(const int &x) const { return x & -x; }
public:
void init() { memset(C, 0, sizeof(C)); }
int sum(int x) const { // 求前缀和S[x]
int ret = 0;
while(x > 0) { ret += C[x], x -= lowbit(x); }
return ret;
}
void add(int x, const int &d) { // 修改索引树,令A[x] += d
while(x < MAXN) {
C[x] += d; x += lowbit(x);
}
}
};