区间信息的维护与查询专题———
树状数组
1.问题来源
动态连续和查询问题。给定一个n个元素的数组A1,A2,...,An,你的任务是设计一个数据结构,支持以下两种操作。
★ Add(x,d)操作:让Ax增加d.
★ Query(L,R):计算AL+AL+1+...+AR.
对普通数组进行一次修改或特定区间求和,时间复杂度为O(N),N为修改或求和需要扫描的数组区间大小。但有一种称为树状数组(又称二叉索引树)的数据结构可以很好的解决这个问题,时间复杂度则为O(logN)。加了一个log,学过数学的我们应该都知道效率优化有多大。
2.相关的定义与概念
在讲实现之前,我们需要先理解一个函数lowbit(x),这是一个自定义的函数,函数名是约定成俗的,作用就是x的二进制表达式中最右边的1所对应的值(而不是这个比特的序号)。比如,38288的二进制是1001010110010000,所以lowbit(38288)=16(二进制是10000)。
代码实现:
int lowbit(int x)//位运算,利用计算机补码特性
{
return x&-x;
}
写个长注释,假设x=10
10的二进制:1010,我们知道 -x就是x的二进制按位取反,末尾加一以后的结果。
-10的二进制:0101+1=0110
然后位运算符&(按位与),1010&0110=10,十进制表示就是2
所以lowbit(10)=2
这个函数很重要,所以先在这里交代清楚原理。
下面上图,就正式开讲树状数组了。
下图是一棵典型的BIT,由15个结点组成,编号为1~15.
灰色结点是树状数组中的结点,每一层结点的lowbit相同,而且lowbit越大,越靠近根。对于结点i,如果它是左子结点,那么父结点的编号就是i+lowbit(i);如果它是右子结点,那么父结点的编号是i-lowbit(i).接着构造一个辅助数组C,其中
Ci=Ai-lowbit(i)+1+Ai-lowbit(i)+2+...+Ai
换句话说,C的每一个元素都是A数组中的一段连续和。在BIT中,每个灰色结点i都属于一个以它自身结尾的水平长条(对于lowbit=1的那些点,“长条”就是那个结点自己),这个长条中的数之和就是Ci。
比如结点12的长条就是从9~12,即C12=A9+A10+A11+A12。同理,C6=A5+A6。这个等式极为重要,请大家花一些时间验证一下“Ci就是以i结尾的水平长条内的元素之和”这一事实。
有了C数组之和,计算前缀和Si就变得简单了。顺着结点i往左走,边走边“往上爬”(注意并不一定沿着树中的边爬),把沿途经过的Ci累加起来。就可以了。(请大家验证,沿途经过的Ci所对应的长条不重复不遗漏地包含了所有需要累加的元素),如下图所示。
而如果修改了一个Ai,需要更新C数组中的哪些元素呢?从Ci开始往右走,边走边“往上爬”(同样不一定沿着树中的边爬),沿途修改所有结点对应的Ci即可(请大家验证,有且仅有这些结点对应的长条包含被修改的元素),如下图所示。
说了那么多,大家发现了吗?树状数组其实就是利用二进制。
3.伪代码实现
树状数组的第i个元素Tree[i]表示A[lowbit(i)+1..i]的和,其中lowbit(i)表示i的最低二进制位。
3.1当想查询一个A[1]+...+A[i]的和,可以依据如下算法:
(1)令sum=0,转第(2)步。
(2)假如i≤0,算法结束,返回sum值,否则sum+=Tree[i],转第(3)步。
(3)i-=lowbit(i),转第(2)步。
可以看出,这个算法就是将一个个区间的和全部加起来,并且i-=lowbit(i)这一步实际上等价于将i的二进制的最后一个1减去,而i的二进制里最多有logn个1,所以查询效率是O(logn).
3.2而给A[i]加上x的算法如下:
(1)当i>n时,算法结束,否则转第(2)步。
(2)Tree[i]+=x,i+=lowbit(i),转第(1)步。
i+=lowbit(i)这个过程实际上是一个把末尾1补为0的过程。容易看出复杂度也是O(logn)
4.模板题讲解
nyoj108士兵杀敌(一)
时间限制:1000 ms | 内存限制:65535 KB
难度:3
描述
南将军手下有N个士兵,分别编号1到N,这些士兵的杀敌数都是已知的。
小工是南将军手下的军师,南将军现在想知道第m号到第n号士兵的总杀敌数,请你帮助小工来回答南将军吧。
注意,南将军可能会问很多次问题。
输入
只有一组测试数据
第一行是两个整数N,M,其中N表示士兵的个数(1<N<1000000),M表示南将军询问的次数(1<M<100000)
随后的一行是N个整数,ai表示第i号士兵杀敌数目。(0<=ai<=100)
随后的M行每行有两个整数m,n,表示南将军想知道第m号到第n号士兵的总杀敌数(1<=m,n<=N)。
输出
对于每一个询问,输出总杀敌数
每个输出占一行
代码如下:
#include<cstdio>
int c[1000005];
int N,M;
int lowbit(int x)
{
return x&(-x);
}
void add(int i,int x)
{
while(i<=N)
{
c[i]=c[i]+x;
i=i+lowbit(i);
}
}
int sum(int i)
{
int sum=0;
while(i>0)
{
sum=sum+c[i];
i=i-lowbit(i);
}
return sum;
}
int main()
{
int i,j,temp,x,y;
scanf("%d%d",&N,&M);
for(i=1;i<=N;i++)
{
scanf("%d",&temp);
add(i,temp);
}
for(i=1;i<=M;i++)
{
scanf("%d%d",&x,&y);
printf("%d\n",sum(y)-sum(x-1));//为什么这里是x-1
}
return 0;
}
5.其他可供练习本算法的题目
nyoj 116 士兵杀敌(二)
nyoj 117 求逆序数
hdu 1166 敌兵布阵
hdu 1556 Color the ball
hdu 1394 Minimum Inversion Number
6.算法延伸
1.二维的树状数组
2.RMQ(范围最值问题)
3.线段树 点修改,区间修改