1.名称
树状数组
2.作用
主要用来求解数列的前缀和,例如:sum[n]=a[0]+a[1]+...+a[n]。
同时能够快速求任意区间的和,例如:设sum(k) = A[1]+A[2]+…+A[k],则A[i] + A[i+1] + … + A[j] = sum(j)-sum(i-1)。
由此引申出三类比较常见问题:
1、单点更新,区间求值-----插点问线。(HDU1166)
2、区间更新,单点求值-----插线问点。(HDU1556)
3、求逆序对。(HDU2838)
3.性能
树状数组中改变某一位,或者求某个区间的和,都是O(logN)。当然用线段树完全可以胜任这些计算,但是线段树写起来代码比较长,并且线段树要占用2*n大小的空间。
4.表示方法
4.1、公式表示
设A[]为一个已知的数列。C[]为树状数组。则会有
C[i]=A[j]+...+A[i];j=i&(-i)=i&(i^(i-1))。
4.2、图形表示
注:
1、最下面的一行表示普通数组A,上面的二进制表示的部分是树状数组C;
2、此图只是将树状数组C与普通数组A的内部意义通过数的形式展示出来。在编程时,树状数组的外观和普通数组一样,只不过存的值得含义不同而已
图片来源于http://hi.baidu.com/rain_bow_joy/blog/item/569ec380c39730d2bc3e1eae.html)
从以上可以发现:
1、树状数组C是表示普通数组A的一部分的和。
2、普通数组A中小标为奇数时,对应的树状数组C[i]中也只能管辖一个A[i];对于偶数下标,则管辖多个。
3、树状数组C中每一个C[i]的最后一个数一定是A[i]。
5.如何求树状数组中的每一个值?(同时也是它的巧妙之处)
5.1、手工计算的时候:
树状数组的巧妙之处在于对于数组下标的二进制的操作,假设a[1...N]为原数组,定义c[1...N]为对应的树状数组:
c[i] = a[i - 2^k + 1] + a[i - 2^k + 2] + ... + a[i - 2^k + 2^k]
其中k为i的二进制表示末尾0的个数,所以2^k即为i的二进制表示的最后一个1的权值.
可以把树状数组看作是把数组分成了若干个2^k大小的空间。对于一个下标i,c[i]的值是由 lowbit(i)个数组元素的值所组成的,每次步进的单位是k=lowbit(i),这个有点像二分归并的思想!这样就可以实现O(log(n))的修改和查询。
例如:
,则由于7对应的二进制末尾有零个0,故c[7]=a[7-2^0+1]=a[7]
,则由于10对应的二进制末尾有1个0,故c[10]=a[10-2^1+1]+ a[10-2^1+2]=a[9]+a[10]
5.2、程序计算的时候:
将每一个普通数组中的元素,通过调用Modify(int num, int v)即可实现对树状数组C中每一个元素的赋值。同时,我们也发现对于树状数组的初始化和修改操作用的是同一个函数。
6.结构的基本操作
int lowbit(int k)
{
return k&(-k);
}
//lowbit函数是计算k的二进制位最低位不为0的数字的个数对应的二次幂。
//例如:
// (7)_10=(111)_2,则通过位运算(7)_10&(-7)_10=(111)_2&(001)_2=1,同时由于7对应的二进制末尾有零个0,故也等于2^0=1
// (8)_10=(1000)_2,,则通过位运算(8)_10&(-8)_10=(1000)_2&(1000)_2=8,同时由于8对应的二进制末尾有3个0,故也等于2^3=8
//(10)_10=(1010)_2 ,则通过位运算(10)_10&(-10)_10=(1010)_2&(0110)_2=2,同时由于10对应的二进制末尾有1个0,故也等于2^1=2
void Modify(int num, int v)
{
while(num <= n)
{
c[num]+=v;
num+=lowbit(num); //改为num+=num&(-num),则可省去lowbit函数
}
}
//Modify函数实现修改数组c中的值,通过函数lowbit更新整个数组的值;
//(7)_10=(111)_2,则通过num+=lowbit(num);依次会得到7、8、16,则依次会更新c[7]、c[8]、c[16]
//(9)_10=(1001)_2,则通过num+=lowbit(num);依次会得到9、10、12、16则依次会更新c[9]、c[10]、c[12]、c[16]
int Sum(int num)
{
int ans=0;
while(num > 0)
{
ans+=c[num];
num-=lowbit(num); //改为num-=num&(-num),则可省去lowbit函数
}
return ans;
}
//Sum函数实现求和,通过函数lowbit查找需要求和用到的树状数组c中的元素,从而间接(因为树状数组和普通数组有如图的对应关系)返回数组元素a[1]+a[2]+…a[num]的和;
//例如:
//树状数组从下标1开始赋值
// (7)_10=(111)_2,,则通过num-=lowbit(num);依次会得到7、6、4、0,则sum(7)=c[7]
// (9)_10=(1001)_2,则通过num-=lowbit(num);依次会得到9,8,0则sum(7)=c[9]+c[8]
然后我们再去做hdu 1166 地兵布阵、hdu 1556 Color the ball、hdu 1838 Chessboard,大家就会有中驾轻就熟的感觉的!
阅读的博客文章
1.http://community.topcoder.com/tc?module=Static&d1=tutorials&d2=binaryIndexedTrees#prob(吐血推荐)
2.http://hi.baidu.com/bnjyjncwbdbjnzr/item/b604bad5b1baa6d8241f40d4
3.http://blog.csdn.net/lulipeng_cpp/article/details/7816527
4.http://mindlee.net/2011/07/10/binary-indexed-trees/
5.http://dongxicheng.org/structure/binary_indexed_tree/
6.http://www.cppblog.com/linyangfei/archive/2008/09/24/62688.html
7.http://www.cnblogs.com/huangxincheng/archive/2012/12/05/2802858.html
8.http://onlinelibrary.wiley.com/doi/10.1002/spe.4380240306/abstract(原始出自于这片论文)