一维树状数组小结

树状数组是一类树状的解决区间统计问题的数据结构。

那么什么是“区间统计”呢?下面举出一个例子:

现有一个长度为n的数组,数组内存有数据。对于这些数据,有2类操作:
  1、修改数据第i个元素。
  2、查询数据一个区间(如p至q)内元素的和。

下面我将使用不同的数据结构解决这个问题,帮助理解数据结构的意义以及树状数组的思想。

方法一:
  使用数组a[i]存储数据的第i个元素。那么对于要求的2种操作:
  1、修改数据第i个元素:直接修改a[i]。时间复杂度为O(1)。
  2、查询数据区间[p,q]的元素和:循环p至q累加。时间复杂度为O(n)。

修改的复杂度是O(1),询问的复杂度是O(N),M次询问的复杂度是M*N.M,N的范围可以有100000以上  

小结:修改操作常数级复杂度,查询操作线性级复杂度。

方法二:
  使用数组a[i]存储数据的第i个元素,并维护一个数组s,s[i]表示数组a前i项的和。如下图:


那么对于要求的2种操作:
  1、修改数据第i个元素:直接修改a[i],但需要维护s,对于s[i..n]进行相应的修改。时间复杂度为O(n)。
  2、查询数据区间[p,q]的元素和:s[q]-s[p]即为所求。时间复杂度为O(1)。

其实如图所示,每修改一个元素i,i之后所有S都要被修改,从下图可以看出,每次迭代步长为1,整个树高为n,所以复杂度为O(n)。向上更新很慢。

  小结:修改操作线性级复杂度,查询操作常数级复杂度。


由上可见,不同的数据结构之间没有绝对的优劣之分,这取决于你的(算法的)需求。  比如,如果你的算法操作一的需求较多,那么使用方法一更优;反之,如果对区间和的询问较多,而修改较少,那么方法二就更好一些了。因此这取决于需求。

下面介绍如何用树状数组解决这个问题

方法三:(树状数组)
  在方法二中,我们需要维护一个数组的前缀和S[i]=A[1]+A[2]+...+A[i]。但是不难发现,如果我们修改了任意一个A[i],S[i]、S[i+1]...S[n]都会发生变化。可以说,每次修改A[i]后,调整前缀和S在最坏情况下会需要O(n)的时间。
  但方法二的思想已经给我们了启发。对于有关“区间”的问题,如果我们只在单个元素上做文章,可能不会有太大的收获。但是如果对于这些数据元素进行合理的划分(如方法二将其化为n个前缀),然后对于整体进行操作,往往会有神奇的功效。

为了使你对树状数组的逻辑结构有一个更为形象的认识,我们先看下面这张图:

      如图所示,红色矩形表示的数组C就是树状数组。这里,C[i]表示A[i-2^k+1]到A[i]的和,而k则是i在二进制时末尾0的个数,或者说是i用2的幂方和表示时的最小指数。

可能这样描述不太适应,换一种方式:


从这两幅图上看,树状数组的树高大约是logn + 1的,所以它更新的速度是O(logn)。从图形上看速度查询和更新的速度应该都是O(logn),综合看来要比前两种方法好。那么怎么求和和更新呢?

下面来看一张数据更多的图:


从图中可以看到若修改Ai,只需修改c[i]上层的元素即可。整个树高log(n)+1,最坏的复杂度为O(log(n))。要求sum(i),则:

Sum[1] = c[1]
Sum[2] = c[2]
Sum[3] = c[3]+c[2]
Sum[4] = c[4]

Sum[5] = c[5]+c[4]
Sum[6] = c[6]+c[4]
Sum[7] = c[7] + c[6] + c[4]
Sum[8] = c[8]

Sum[9] = c[9]+c[8]
Sum[10] = c[10]+c[8]
Sum[11] = c[11]+c[10] + c[8]
Sum[12] = c[12]+c[8]

Sum[13] = c[13] + c[12] + c[8]
Sum[14] = c[14] + c[12] + c[8]
Sum[15] = c[15] + c[14] + c[12] + c[8]
Sum[16] = c[16]


1、若i是2^k方,则C[i]= sum[i]
2、i不是2幂次方,那么:
sum(i) = c[i] + c[i = i-lowbit(i)] + ……c[i= 2^k]

     树状数组就是利用数的二进制形式的特性,来定制步长,这里是不容易理解的

     二进制的形式与一个数用2的幂方和表示的关系大家应该都清楚,比如:
     23的二进制为10111,即23 = 1*2^4+0*2^3+1*2^2+1*2^1+1*2^0
     即23用2的幂方和表示为2^4+2^2+2^1+2^0。

     即23用2的幂方和表示的2的最小指数为0,那么C[23]表示A[23-2^0+1]..A[23]的元素和,即A[23]。

     我们再举出一个例子:比如6的二进制为110,则对于6的k为1(1为6的2的幂方和表示2的最小指数)。那么C[6]表示A[6-2*1+1]..A[6]的和,即C[6]=A[5]+A[6]。

    继续观察这个图,我们可以看到,所谓的k,也是该节点在树中的高度。下面,我们归回到最开始的问题

     对于一开始要求的2种操作:
     1、修改第i个元素:
     从图示中我们可以看出,修改第i个元素,为了维护数组C的意义,需要修改C[i]以及C[i]的全部祖先,而非C[i]的祖      先的节点则对于第i个元素的修改,不会发生改变。(想想看为什么) 那么修改C[i]的全部祖先有多少个?从图示中可以看出,C[i]的祖先共有“树的高度 - C[i]节点高度”个,而有n个元素的树状数组的高度为[Log2N](在以后的算法分析中,忽略对数函数的底数)。在元素修改过程中,修改C[i]并沿着树型结构一直向上回溯修改到树根,那么对于修改元素的操作需要修改树状数组中的LogN个节点,即时间复杂度为O(Log N)。

     2、区间[p,q]元素和查询
     刚才我们把数据进行了划分,其目的很显然,就是在对区间进行统计的时候可以对整体进行统计不是一个一个统计。对于其他的用于区间统计问题的数据结构大多也是这样的思路。
     要求区间[p,q]元素和,可求[1,q]、[1,p]作差。则问题转化为如何查询一个区间[1,p]的元素和,即求s[p]。回顾一下我们是如何划分树状数组的区间的,对于求数列的前n项和,只需找到n以前的所有最大子树,把其根节点的C加起来即可。不难发现,这些子树的数目是n在二进制时1的个数,或者说是把n展开成2的幂方和时的项数。因此,求和操作的复杂度也是O(logn)。


举个例子说明,还是以23为例: 23的二进制为10111,即
   10111(2) = 23 C[23] = A[23]
   10110(2) = 22 C[22] = A[21] + A[22]
   10100(2) = 20 C[20] = A[17] + ... + A[20]
   10000(2) = 16 C[16] = A[1] + ... + A[16]
  则S[23] = C[16] + C[20] + C[22] + [23]
  易见,对于任意p,求s[p]只需将若干树状数组上节点c进行加和即可,因为所加节点个数等于p的二进制形式中1的个数,因此统计过程的复杂度为O(Log N)。  

综上所述,使用树状数组解决这个问题,修改操作对数级复杂度,查询操作也为对数级复杂度。也就是说,方法三相对于方法一和方法二,更适合解决修改和查询操作次数差不多的情况

现在说明一下树状数组各部分的实现

假设一维数组为A[i](i=1,2,...n),则与它对应的树状数组C[i](i=1,2,...n)是这样定义的:

C1 = A1
C2 = A1 + A2
C3 = A3
C4 = A1 + A2 + A3 + A4
C5 = A5
C6 = A5 + A6
C7 = A7
C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8
……
C16 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8 + A9 + A10 + A11 + A12 + A13 + A14 + A15 + A16
......

(1)C[x]展开以后是什么呢?有多少项?根据上面讲述由下面公式计算:

int lowbit(int x){//计算c[t]展开的项数  
   return x&(-x);  
  }
 
C[x]展开的项数就是lowbit(x),C[x]就是从A[x]开始往左连续求lowbit(x)个数的和.

这一部分比较难理解,现在简单讲述一下:

通过上面的介绍,可以发现,实现树状数组的关键,在于求一个数x的二进制时末尾0的个数k(用2的幂方和表示时的最小指数)。而2^k就是修改(和统计)时指针滑动的距离,我们定义这个值为x的lowbit.更具体的说,正整数x的lowbit为将x二进制中最后一个1按位抽取的结果。比如,23(10111)的lowbit为1(00001),20(10100)的lowbit为4(00100)

那么现在问题就转化为求解如何求解lowbit?  

   如何按位抽取一个数二进制的最后一个1呢?这里介绍如何用位运算来解决这个问题,并利用有符号整数补码的存储规则,将这个过程书写的更加简练。
  对于一个二进制数p,以111010000为例。
  将这个数减1,即p-1为111001111。
  将这两个数取异或,即p^(p-1),为000011111。
  将原数与这个数取和,即p&(p^(p-1)),为000010000。
  至此已经成功将p二进制的最后一个1按位抽取出来了。

        lowbit(p) = p & ( p ^ ( p - 1 ) )
  根据有符号整数的补码规则,我们可以发现(p^(p-1))恰好等于-p,即lowbit的求取公式可以更为简练:
        lowbit(p) = p & -p

(2)修改
    比如修改了A3,必须修改C3,C4,C8,C16,C32,C64...
    当我们修改A[i]的值时,可以从C[i]往根节点一路上溯,调整这条路上的所有C[]即可,对于节点i,父节点下标 p=i+lowbit(i).给A[i]加上 x后,更新一系列C[j].

void UFset(int pos, int data)
{
     while(pos <= MAXN)
     {
          c[pos] += data;
          pos += lowbit( pos );
     }
}

(3)求数列A[]的前n项和,只需找到n以前的所有最大子树,把其根节点的C加起来即可。
如:Sun(1)=C[1]=A[1];
      Sun(2)=C[2]=A[1]+A[2];
      Sun(3)=C[3]+C[2]=A[1]+A[2]+A[3];
      Sun(4)=C[4]=A[1]+A[2]+A[3]+A[4];
      Sun(5)=C[5]+C[4];
      Sun(6)=C[6]+C[4];
      Sun(7)=C[7]+C[6]+C[4];
      Sun(8)=C[8];
...................................

int Querry( int pos )
{
     int sum = 0;
     while( pos > 0 )
     {
          sum += c[pos];
          pos -= lowbit( pos );
     }
     return sum;
}


至此一位数组的实现也结束了

int lowbit( int x )
{
     return x & (-x);
}

void UFset(int pos, int data)
{
     while(pos <= MAXN)
     {
          c[pos] += data;
          pos += lowbit( pos );
     }
}

int Querry( int pos )
{
     int sum = 0;
     while( pos > 0 )
     {
          sum += c[pos];
          pos -= lowbit( pos );
     }
     return sum;
}



 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值