二分索引树与线段树分析

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二分索引树与线段树分析相关的知识,希望对你有一定的参考价值。

  二分索引树是一种树状数组,其全名为Binary Indexed Tree。二分索引树可以用作统计作用,用于计某段连续区间中的总和,并且允许我们动态变更区间中存储的值。二分索引树和线段树非常相似,二者都享有相同的O(log2(n))时间复杂度的更新操作和O(log2(n))时间复杂度的查询操作,区别在于二分索引树更加简洁高效,而线段树则较冗杂低效,原因在于对二分索引树的操作中是使用了计算机中整数存储的特性来进行加速,而线段树中由于使用的是比较操作,因此性能不及二分索引树。那么为什么我们不抛弃线段树呢?原因在于所有二分索引树能解决的问题,线段树也都可以解决,但是线段树还能解决许多二分索引树无法解决的问题。下面将先后讨论二分索引树和线段树。


  要叙述二分索引树,我们必须先说明二分索引树高效的关键,lowbit操作,其中lowbit(x)=x&-x。我们知道在计算机中,对于一个带符号n位整数b,其由n个二进制位表示,可以记为(b[n-1],b[n-2],...,b[0]),其中对于任意0<=i<n,b[i]为0或1。而一个这样的向量,其实际表示的整数值为$$ value\left(b\left[n-1\right],\cdots ,b\left[0\right]\right)=-b\left[n-1\right]\cdot 2^{n-1}+\sum_{i=0}^{n-2}{b\left[i\right]\cdot 2^i} $$对于一个任意正整数X,我们先对其按位取反(除了首位),得到一个新的数Y,很容易可以得知X+Y=2^(n-1)-1,因此X+Y+1=2^(n-1),换言之Y+1-2^(n-1)=-X,因此我们得到-X的二进制表示,其后n-1位为X按位取反后加1得到的(与Y+1的后n-1位相同),且首位为1。接下来考虑X&-X的实际含义,注意由于X为非负正数,因此X的首位为0,而运算为且运算,因此我们可以忽略-X的符号位带来的影响。我们不妨认为X的后面k位均为0,且X[k]=1,对应的Y的后面k位均为1,而Y[k]=0。而Y+1的后面k位均为0,而(Y+1)[k]=1,之后的前面n-2-k位与Y一致,与X相反。因此X&-X得到的值应该为2^k,到此我们可以得出一个结论X&-X得到的数为以二进制视角从后数起第一个为1的二进制位所代表的整数值。事实上这个结论对于X为0时也是成立的,并且由于且运算满足交换律,因此当X为负数时也可以得到正确结果,这些只是补充说明而已,我们要用到的只是lowbit应用到正整数情况下表现出的性质而已。

  接下来我们用二分索引树表示一段长度为L的数组A,且从1开始计数,即我们认为下标分别为1,2,...,L。二分索引树支持两种操作,修改某个A[i]的值,以及查询S[j]=A[1]+A[2]+...+A[j]的加总和,两种操作允许以任意次序交错执行。

  不难知道使用原始数组存取,我们的查询时间复杂度将达到O(L),在查询密集的情况下是一个噩梦,而如果我们缓存S[j]的值,这样每次更新的时间复杂度将达到O(L),这也是不允许的。我们可以利用一个精致的机制,我们首先定义一个长度为L的数组data,其中data[i]表示S[i]-S[i-lowbit(i)]的值,换言之,data[i]表示A在区间(i-lowbit[i],i]范围内所有元素的加总值。这样我们的查询操作可以用下面的伪代码实现:

query(x)//查询A[1]+...+A[x]
    sum = 0
    for(i = x; i > 0; i = i - lowbit(i))
        sum = sum + data[i]
    return sum

  我们定义函数f(x):=x-lowbit(x)。

  在整个流程中我们汇总了下标为f^0(x),f^1(x),...,f^k(x)的data中的值,我们对其进行加总得到:$$ data\left[f^0\left(x\right)\right]+data\left[f^1\left(x\right)\right]+\cdots +data\left[f^k\left(x\right)\right] $$ $$ =S\left[f^0\left(x\right)\right]-S\left[f^1\left(x\right)\right]+S\left[f^1\left(x\right)\right]-S\left[f^2\left(x\right)\right]+\cdots +S\left[f^k\left(x\right)\right]-S\left[0\right] $$ $$ =S\left[f^0\left(x\right)\right]=S\left[x\right] $$

  到此我们说明了query返回的结果是正确的,而时间复杂度,我们每次调用函数f都会导致i最靠后的为1的二进制位被修改为0,而i中最多有log2(i)个为1的二进制位,因此时间复杂度为O(log2(i)),在i=L的情况下达到最大O(log2(L))。

  说明完了查询,下面说明如何存储数据。当我们修改了A[i]中的值,我们势必需要修正所有满足f(x)<i<=x的x对应的data[x]中存储的值,以保证data中数据的正确。

  命题1:对于一个给定数k和i,最多只存在一个同时满足lowbit(x)=2^k且f(x)<i<=x的数x。

  证明:假设x与y为两个不同的满足条件的值,则有x-2^k<i<=x与y-2^k<i<=y成立,不妨设x<y,则有y-x>2^k(由于2^k同时是x与y的约数,后面不再说明),而y-2^k>x>i>y-2^k将成立,这是不可能的,因此命题成立。

  命题2:若两个不同的数x,y同时满足f(x)<i<=x,f(y)<i<=y,则由lowbit(x)<lowbit(y)可以得出x<y。

  证明:记2^p=lowbit(x),则x>y可以得出x-y>=2^p,即i>x-2^p>=y,这与前提相悖,因此命题成立。

  命题3:若某个数x满足f(x)<i<=x,且i的后面t个二进制位为0,且i[t]=1,则lowbit(x)>=2^t

  证明:设lowbit(x)=2^p<2^t,我们可以记y为x将后面t位均设置为0后的值,因此有y+2^t>x>=i,即i-y<2^t,从而可得i<=y,而x-2^p>=y>=i,因此命题成立。

  命题4:若某个数x满足f(x)<i<=x,那么y=x+lowbit(x)是大于x中满足f(y)<i<=y的最小下标

  证明:证明y=x+lowbit(x)满足f(y)<i<=y比较简单,这里不证明。而对于任意z,若z>x,且f(z)<i<=z,则根据命题3可知lowbit(z)>lowbit(x),由此可知z-x>=lowbit(x),即z>=lowbit(x)+x=y。

  这里我们定义g(x)=x+lowbit(x)。由上面4个命题可以得出一系列需要更新的下标从小到大排序为g^0(x),g^1(x),...,g^k(x)。因此我们给出下面代码:

update(j, val) //令A[j]增加val
    for(i = j; i <= L; i = i + lowbit(i))
        data[i] = data[i] + val

  这里使用的是加法,和前面提及的直接赋值有所不同,但是可以将直接对A[j]赋值v对应的修改为令A[j]增加v-A[j]即可。

  由于i在每次循环,其lowbit值不断增大,因此update的时间复杂度也是log2(L)。


  线段树有空再补上...

 

以上是关于二分索引树与线段树分析的主要内容,如果未能解决你的问题,请参考以下文章

2653: middle

线段树与树状数组的对比应用

ZZNU-OJ-2098 : Drink coffee线段树合并区间或者 差分 + 二分索引树

菜鸡2014X的数据结构学习小结:线段树与树状数组

线段树与位运算

探索B树/B+树与MySQL数据库索引的关系