树状数组解析

来源:互联网 发布:linux怎么进入man 编辑:程序博客网 时间:2024/06/05 17:56

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(原始出自于这片论文)

原创粉丝点击