引言
在解题过程中,我们有时需要维护一个数组的前缀和S[i]=A[i]+A[2]+...+A[i].但是不难发现,如果我们修改了任意一个A[i],S[i],S[i+1]...S[n]都会发生变化。可以说,每次修改A[i]后,调整前缀和S在最坏的情况下会需要O(n)的时间。当n非常大时,程序会运行得非常慢。因此,这里我们引入“树状数组”,它的修改与求和都是O(logn),效率非常高。
基本思想
根据任意正整数关于2的不重复次幂的唯一分解性质,若一个正整数x的二进制表示为10101,其中等于1的位是0,2,4,则正整数x可以被"二进制分解成2^4+2^2+2^0"。进一步地,区间[1,x]可以分成O(logx)个小区间:
1.长度为2^4的小区间[1,2^4]
2.长度为2^2的小区间[2^4+1,2^4+2^2]
3.长度为2^0的小区间[2^4+2^2+1,2^4+2^2+2^0]
树状数组就是一种基于上述思想的数据结构,其基本用途是维护序列的前缀和。对于区间[1,x],树状数组将其分为logx个子区间,从而满足快速询问区间和。
基本算法
由上文可知,这些子区间的共同特点是:若区间结尾为R,则区间长度就等于R的“二进制分解”下最小的2次幂,我们设为lowbit(R)。
对于给定的序列A,我们建立一个数组c,其中c[x]保存序列A的区间[x-lowbit(x)+1,x]中所有数的和。
事实上,数组c可以看做一个如下图所示的树形结构。
该结构满足以下性质:
1.每个内部结点c[x]保存以它为根的子数中所有叶节点的和
2.每个内部结点c[x]的子节点个数等于lowbit(x)的大小
3.除树根外,每个内部结点c[x]的父结点c[x+lowbit(x)]
4.数的深度为O(logN)
求lowbit(x)
lowbit(n)表示取出非负整数n在二进制表示下最低位的1以及它后边的0构成的数值。
lowbit(n)=n&(-n),这是怎么得来的?具体分析如下:
设n>0,n的第K位是1,第0~k-1位都是0.
为了实现lowbit运算,先把n取反,此时第k位变为0,第0~k-1位都是1.再令n=n+1,此时因为进位,第k位变为1,第0~k-1位都是0.在上面的取反操作后,n的第k+1到最高位恰好与原来相反,所以n&(~n+1)仅有第k位为1,其余位为0.而在补码表示下,~n=-1-n,因此lowbit(n)=n&(-n).
代码实现
int lowbit(int n)
{
return n&(-n);
}
对某个元素进行加法操作
树状数组支持单点增加,意思是给序列中的某个数A[x]加上y,同时正确维护序列的前缀和。
根据上面给出的树形结构和它的性质,只有结点c[x]及其所有祖先结点保存的"区间和"包含A[x],而任意一个结点的祖先至多只有logN个,我们逐一对它们的数值c值进行更新即可。下面的代码在O(logN)时间内执行单点增加操作。
代码实现
void update(int x,int y)
{
for(;x<=N;x+=x&-x)
c[x]+=y;
}
查询前缀和
树状数组支持查询前缀和,即序列A第1~x个数的和。按照我们刚才提出方法,应该求出x的二进制表示中每个等于1的位。把[1,x]分成O(logN)个小区间,而每个小区间的区间和都已经保存在数组c中。下面的代码是在O(logN)时间内查询前缀和:
int sum(int x)
{
int ans=0;
for(;x;x-=x&-x)
ans+=c[x];
return ans;
}
统计A[x]....A[y]的值
调用以上的sum操作:sum(y)-sum(x-1)
扩展(多维数组)
一维的树状数组的每个操作的复杂度都是O(logn)的,非常高效。它可以扩充为m维这样每个操作的复杂度就变成了O(log^mn),在m不大的时候,时间复杂度是可以接受的。扩充的方法就是将原来的修改和查询函数中的一个循环,改成m个循环m维数组c中的操作。也就是说,如果有n*m的二维数组a,树状数组为c,那么单点修改操作为:
int update(int x,int y,int z)
{
int i=x;
while(i<=n)
{
int j=y;
while(j<=m)
{
c[i][j]+=z;
j+=lowbit(j);
}
i+=lobit(i);
}
}
int sum(int x,int y)
{
int re=0,i=x;
while(i>0)
{
int j=y;
while(j>0)
{
re+=c[i][j];
j-=lowbit(j);
}
i-=lowbit(i);
}
return re;
}
注意事项
要注意树状数组能处理的是下标为1...n的数组,绝不能出现下边为0的情况。因为lowbit(0)=0,这样会陷入死循环。
摘自《信息学奥赛一本通 提高篇》