首页 > 代码库 > 树状数组

树状数组

树状数组

第01讲 什么是树状数组?

树状数组用来求区间元素和,求一次区间元素和的时间效率为O(logn)。有些同学会觉得很奇怪。用一个数组S[i]保存序列A[]的前i个元素和,那么求区间i,j的元素和不就为S[j]-S[i-1],那么时间效率为O(1),岂不是更快?但是,如果题目的A[]会改变呢?例如:我们来定义下列问题:我们有n个盒子。可能的操作为:

1.向盒子k添加石块

2.查询从盒子i到盒子j总的石块数

自然的解法带有对操作1为O(1)而对操作2为O(n)的时间复杂度。但是用树状数组,对操作1和2的时间复杂度都为O(logn)。

第02讲 图解树状数组C[]

现在来说明下树状数组是什么东西?假设序列为A[1]~A[8]

 

网络上面都有这个图,但是我将这个图做了2点改进。

(1)图中有一棵满二叉树,满二叉树的每一个结点对应A[]中的一个元素。

(2)C[i]为A[i]对应的那一列的最高的节点。

现在告诉你:序列C[]就是树状数组。

那么C[]如何求得?

C[1]=A[1];

C[2]=A[1]+A[2];

C[3]=A[3];

C[4]=A[1]+A[2]+A[3]+A[4];

C[5]=A[5];

C[6]=A[5]+A[6];

C[7]=A[7];

C[8]= A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];

以上只是枚举了所有的情况,那么推广到一般情况,得到一个C[i]的抽象定义:因为A[]中的每个元素对应满二叉树的每个叶子,所以我们干脆把A[]中的每个元素当成叶子,那么:C[i]=C[i]的所有叶子的和。现在不得不引出关于二进制的一个规律:

先仔细看下图:



 

将十进制化成二进制,然后观察这些二进制数最右边1的位置:

1 --> 00000001

2 --> 00000010

3 --> 00000011

4 --> 00000100

5 --> 00000101

6 --> 00000110

7 --> 00000111

8 --> 00001000

1的位置其实从我画的满二叉树中就可以看出来。但是这与C[]有什么关系呢?接下来的这部分内容很重要:在满二叉树中,

以1结尾的那些结点(C[1],C[3],C[5],C[7]),其叶子数有1个,所以这些结点C[i]代表区间范围为1的元素和;

以10结尾的那些结点(C[2],C[6]),其叶子数为2个,所以这些结点C[i]代表区间范围为2的元素和;

以100结尾的那些结点(C[4]),其叶子数为4个,所以这些结点C[i]代表区间范围为4的元素和;

以1000结尾的那些结点(C[8]),其叶子数为8个,所以这些结点C[i]代表区间范围为8的元素和。

扩展到一般情况:

i的二进制中的从右往左数有连续的x个“0”,那么拥有2^x个叶子,为序列A[]中的第i-2^x+1到第i个元素的和。终于,我们得到了一个C[i]的具体定义:

C[i]=A[i-2^x+1]+…+A[i],其中x为i的二进制中的从右往左数有连续“0”的个数。

第03讲 利用树状数组求前i个元素的和S[i]

理解了C[i]后,前i个元素的和S[i]就很容易实现。从C[i]的定义出发:

C[i]=A[i-2^x+1]+…+A[i],其中x为i的二进制中的从右往左数有连续“0”的个数。

我们可以知道:C[i]是肯定包括A[i]的,那么:S[i]=C[i]+C[i-2^x]+…

也许上面这个公式太抽象了,因为有省略号,我们拿一个具体的实例来看:

S[7]=C[7]+C[6]+C[4]

因为C[7]=A[7],C[6]=A[6]+A[5],C[4]=A[4]+A[3]+A[2]+A[1],所以S[7]=C[7]+C[6]+C[4]

(1)i=7,求得x=0,那么我们求得了A[7];

(2)i=i-2^x=6,求得x=1,那么求得了A[6]+A[5];

(3)i=i-2^x=4,求得x=2,那么求得了A[4]+A[3]+A[2]+A[1]。

讲到这里其实有点难度,因为S[i]的求法,如果要讲清楚,那么得写太多的东西了。所以不理解的同学,再反复多看几遍。

从(1)(2)(3)这3步可以知道,求S[i]就是一个累加的过程,如果将2^x求出来了,那么这个过程用C++实现就没什么难度。现在直接告诉你结论:2^x=i&(-i)

证明:设A’为A的二进制反码,i的二进制表示成A1B,其中A不管,B为全0序列。那么-i=A’0B’+1。由于B为全0序列,那么B’就是全1序列,所以-i=A’1B,所以:i&(-i)= A1B& A’1B=1B,即2^x的值。

所以根据(1)(2)(3)的过程我们可以写出如下的函数:

int Sum(int i) //返回前i个元素和

{

       int s=0;

       while(i>0)

       {

              s+=C[i];

              i-=i&(-i);

       }

       return s;

}

 

第04讲 更新C[]

正如第01讲提到的小石块问题,如果数组A[i]被更新了怎么办?那么如何改动C[]?如果改动C[]也需要O(n)的时间复杂度,那么树状数组就没有任何优势。所以树状数组在改动C[]上面的时间效率为O(logn),为什么呢?因为改动A[i]只需要改动部分的C[]。这一点从第02讲的图中就可以看出来:



 

如上图:假如A[3]=3,接着A[3]+=1,那么哪些C[]需要改变呢?

答案从图中就可以得出:C[3],C[4],C[8]。因为这些值和A[3]是有联系的,他们用树的关系描述就是:C[3],C[4],C[8]是A[3]的祖先。那么怎么知道那些C[]需要变化呢?我们来看“A”这个结点。这个“A”结点非常的重要,因为他体现了一个关系:A的叶子数为C[3]的2倍。因为“A”的左子树和右子树的叶子数是相同的。因为2^x代表的就是叶子数,所以C[3]的父亲是A,A的父亲是C[i+2^0],即C[3]改变,那么C[3+2^0]也改变。我们再来看看“B”这个结点。B结点的叶子数为2倍的C[6]的叶子数。所以B和C[6+2^1]在同一列,所以C[6]改变,C[6+2^1]也改变。

推广到一般情况就是:

如果A[i]发生改变,那么C[i]发生改变,C[i]的父亲C[i+2^x]也发生改变。这一行的迭代过程,我们可以写出当A[i]发生改变时,C[]的更新函数为:

void Update(int i,int value)  //A[i]的改变值为value

{

       while(i<=n)

       {

              C[i]+=value;

              i+=i&(-i);

       }

}


看完这些就可以去做水题练练手了。推荐入门题 HDU 1166   敌兵布阵  HOJ 1867 经理的烦恼  高桥和低桥    HDU 1556  Color the ball    


第05讲 一维树状数组的应用举例

废了4讲的话,我们终于把一维树状数组的2个不到5行的代码给搞定了。现在要正式投入到应用当中。

POJ 2352

题意:按照y升序给你n个星星的坐标,如果有m个星星的x,y坐标均小于等于星星A的坐标,那么星星A的等级为m。

分析:是一道树状数组题。举例来说,以下是题目的输入:

5

1 1

5 1

7 1

3 3

5 5

由于y坐标是升序的且坐标不重复,所以在星星A后面输入的星星的x,y坐标不可能都小于等于星星A。假如当前输入的星星为(3,3),易得我们只需要去找树状数组中小于等于3的值就可以了,即GetSum(3)。注意:A[i]表示x坐标为i的个数,C[]为A[]的树状数组,那么GetSum(i)就是序列中前i个元素的和,即x小于等于i的星星数。

本题还是一点要注意:星星坐标的输入可以是(0,0),所以我们把x坐标统一加1,然后用树状数组实现。

第06讲 二维树状数组

BIT可用为二维数据结果。假设你有一个带有点的平面(有非负的坐标)。你有三个问题:

1.在(x , y)设置点

2.从(x , y)移除点

3.在矩形(0 , 0), (x , y)计算点数 - 其中(0 , 0)为左下角,(x , y)为右上角,而边是平行于x轴和y轴。

对于1操作,在(x,y)处设置点,即Update(x,y,1),那么这个Update要怎么写?很简单,因为x,y坐标是离散的,所以我们分别对x,y进行更新即可,函数如下:

void Update(int x,int y,int val)

{

       while(x<=n)

       {

              int y1=y;

              while(y1<=n)

              {

                     C[x][y1]+=val;

                     y1+=y1&(-y1);

              }

              x+=x&(-x);

       }

}

那么根据Update可以推得:GetSum函数为:

int GetSum(int x,int y)

{

       int sum=0;

       while(x>0)

       {

              int y1=y;

              while(y1>0)

              {

                     sum+=C[x][y1];

                     y1-=y1&(-y1);

              }

              x-=x&(-x);

       }

       return sum;

}

第07讲 二维树状数组的应用举例

POJ  2215

我们先讨论POJ2155的一维情况,如下:

有一个n卡片的阵列。每个卡片倒放在桌面上。你有两个问题:

  1. T i j (反转从索引i到索引j的卡片,包括第i张和第j张卡——面朝下的卡将朝上;面朝上的卡将朝下)

  2. Q i (如果第i张卡面朝下回答0否则回答1)

解决:

解决问题(1和2)的方法有时间复杂度O(log n)。在数组f(长度n + 1)我们存储每个问题T(i, j)——我们设置f[i]++和f[j + 1]--。对在i和j之间(包括i和j)每个卡k求和f[1] + f[2] + ... + f[k]将递增1,其他全部和前面的一样(看图2.0清楚一些),我们的结果将描述为和(和累积频率一样)模2。



 

图 2.0

使用BIT来存储(增加/减少)频率并读取累积频率。

理解了一维的情况,POJ2155就是其二维的版本,易得只需要更(x1,y1),(x1,y2+1),(x2+1,y1),(x2+1,y2+1)四个点的C[]的值就可以了,最后的结果依然是GetSum(x,y)%2

 

POJ 2352的代码如下:

 

Cpp代码  收藏代码
  1. #include<iostream>  
  2. using namespace std;  
  3. const int NMAX = 15005;  
  4. const int MMAX = 32005;  
  5.   
  6.   
  7.   
  8. int n, ar[MMAX], lev[NMAX];  
  9.   
  10. int lowbit(int i)  
  11. {  
  12.     return i&(-i);  
  13. }  
  14.   
  15. void add(int i)  
  16. {  
  17.     while(i <= MMAX)  
  18.     {          //  这里不能用n,要区别开来。  
  19.         ar[i] += 1;  
  20.         i += lowbit(i);  
  21.     }  
  22. }  
  23.   
  24. int sum(int i)  
  25. {  
  26.     int ans = 0;  
  27.     while(i > 0)  
  28.     {  
  29.         ans += ar[i];  
  30.         i -= lowbit(i);  
  31.     }  
  32.     return ans;  
  33. }  
  34.   
  35. int main()  
  36. {  
  37.     int i, x, y;  
  38.     scanf("%d", &n);  
  39.     for(i = 0; i < n; i ++)  
  40.     {  
  41.         scanf("%d%d", &x, &y);  
  42.         lev[sum(++x)] ++;      // 一定要++x,以后要注意下,WA在了这。  
  43.         add(x);  
  44.     }  
  45.     for(i = 0; i < n; i ++)  
  46.         printf("%d\n", lev[i]);  
  47.     return 0;  
  48. }