• (转载)树状数组


    参考资料:

         [1、Beyond the Void - 树状数组][1]
         [2、夜深人静写算法(三) - 树状数组][2]

    树状数组(Binary Index Tree)亦即二进制索引树,一种通过二进制位来维护一个序列前i和的数据结构。

    1、树 or 数组 ?

    名曰树状数组,那么究竟它是树还是数组呢?数组在物理空间上是连续的,而树是通过父子关系关联起来的,而树状数组正是这两种关系的结合,首先在存储空间上它是以数组的形式存储的,即下标连续;其次,对于两个数组下标x,y(x < y),如果x + 2^k = y (k等于x的二进制表示中末尾0的个数),那么定义(y, x)为一组树上的父子关系,其中y为父结点,x为子结点。

    如图二-1-1,其中A为普通数组,C为树状数组(C在物理空间上和A一样都是连续存储的)。树状数组的第4个元素C4的父结点为C8 (4的二进制表示为"100",所以k=2,那么4 + 2^2 = 8),C6和C7同理。C2和C3的父结点为C4,同样也是可以用上面的关系得出的,那么从定义出发,奇数下标一定是叶子结点。

    2、结点含义

    然后我们来看树状数组上的结点Ci具体表示什么,这时候就需要利用树的递归性质了。我们定义Ci的值为它的所有子结点的值 和 Ai 的总和,之前提到当i为奇数时Ci一定为叶子结点,所以有Ci = Ai ( i为奇数 )。从图中可以得出:

      C1 = A1
      C2 = C1 + A2 = A1 + A2
      C3 = A3
      C4 = C2 + C3 + A4 = A1 + A2 + A3 + A4
      C5 = A5
      C6 = C5 + A6 = A5 + A6
      C7 = A7
      C8 = C4 + C6 + C7 + A8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8
    
    建议直接看C8,因为它最具代表性。
    我们从中可以发现,其实Ci还有一种更加普适的定义,它表示的其实是一段原数组A的连续区间和。根据定义,右区间是很明显的,一定是i,即Ci表示的区间的最后一个元素一定是Ai,那么接下来就是要求Ci表示的第一个元素是什么。从图上可以很容易的清楚,其实就是顺着Ci的最左儿子一直找直到找到叶子结点,那个叶子结点就是Ci表示区间的第一个元素。
    更加具体的,如果i的二进制表示为 ABCDE1000,那么它最左边的儿子就是 ABCDE0100,这一步是通过结点父子关系的定义进行逆推得到,并且这条路径可以表示如下:
    
            ABCDE1000     =>     ABCDE0100     =>     ABCDE0010     =>     ABCDE0001    
    
    这时候,ABCDE0001已经是叶子结点了,所以它就是Ci能够表示的第一个元素的下标,那么我们发现,如果用k来表示i的二进制末尾0的个数,Ci能够表示的A数组的区间的元素个数为2^k,又因为区间和的最后一个数一定是Ai,所以有如下公式:
    
        Ci  =  sum{ A[j] |  i - 2^k + 1 <= j <= i }   (帮助理解:将j的两个端点相减+1 等于2^k) 
    

    3、求和操作

    明白了Ci的含义后,我们需要通过它来求sum{ A[j] | 1 <= j <= i },也就是之前提到的sum(i)函数。为了简化问题,用一个函数lowbit(i)来表示2^k (k等于i的二进制表示中末尾0的个数)。那么:

    sum(i) = sum{ A[j] | 1 <= j <= i }
    = A[1] + A[2] + ... + A[i]
    = A[1] + A[2] + A[i-2^k] + A[i-2^k+1] + ... + A[i]
    = A[1] + A[2] + A[i-2^k] + C[i]
    = sum(i - 2^k) + C[i]
    = sum( i - lowbit(i) ) + C[i]

    由于C[i]已知,所以sum(i)可以通过递归求解,递归出口为当i = 0时,返回0。sum(i)函数的函数主体只需要一行代码:
    

    int sum(int x)
    {
    return x ? C[x]+ sum( x - lowbit(x)):0;
    }

    观察 i - lowbit(i),其实就是将i的二进制表示的最后一个1去掉,最多只有log(i)个1,所以求sum(n)的最坏时间复杂度为O(logn)。由于递归的时候常数开销比较大,所以一般写成迭代的形式更好。写成迭代代码如下:
    

    int sum(int x)
    {
    int s =0;
    for(int i = x; i ; i -= lowbit(i))
    {
    s += c[i][j];
    }
    return s;
    }

    4、更新操作

    更新操作就是之前提到的add(i, 1) 和 add(i, -1),更加具体得,可以推广到add(i, v),表示的其实就是 A[i] = A[i] + v。但是我们不能在原数组A上操作,而是要像求和操作一样,在树状数组C上进行操作。
    那么其实就是求在Ai改变的时候会影响哪些Ci,看图二-1-1的树形结构就一目了然了,Ai的改变只会影响Ci及其祖先结点,即A5的改变影响的是C5、C6、C8;而A1的改变影响的是C1、C2、C4、C8。
    也就是每次add(i, v),我们只要更新Ci以及它的祖先结点,之前已经讨论过两个结点父子关系是如何建立的,所以给定一个x,一定能够在最多log(n) (这里的n是之前提到的值域) 次内更新完所有x的祖先结点,add(i, v)的主体代码(去除边界判断)也只有一行代码:

    void add(int x,int v)
    {
    if(x <= n)
    {
    C[x]+= v, add( x + lowbit(i), v );
    }
    }

    和求和操作类似,递归的时候常数开销比较大,所以一般写成迭代的形式更好。写成迭代形式的代码如下:
    

    void add(int x,int v)
    {
    for(int i = x; i <= n; i += lowbit(i))
    {
    C[i] += v;
    }
    }

    
    ##5、lowbit函数O(1)实现
    >上文提到的两个函数sum(x)和add(x, v)都是用递归实现的,并且都用到了一个函数叫lowbit(x),表示的是2k,其中k为x的二进制表示末尾0的个数,那么最简单的实现办法就是通过位运算的右移,循环判断最后一位是0还是1,从而统计末尾0的个数,一旦发现1后统计完毕,计数器保存的值就是k,当然这样的做法总的复杂度为O( logn ),一个32位的整数最多可能进行31次判断(这里讨论整数的情况,所以符号位不算)。
    >这里介绍一种O(1)的方法计算2k的方法。
    >来看一段补码小知识:
    >清楚补码的表示的可以跳过这一段,计算机中的符号数有三种表示方法,即原码、反码和补码。三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位,三种表示方法各不相同。这里只讨论整数补码的情况,在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理。整数补码的表示分两种:
    >&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;正数:正数的补码即其二进制表示;
    >&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;例如一个8位二进制的整数+5,它的补码就是 00000101 (标红的是符号位,0表示"正",1表示“负”)
    >&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;负数:负数的补码即将其整数对应的二进制表示所有位取反(包括符号位)后+1;
    >&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;例如一个8位二进制的整数-5,它的二进制表示是00000101,取反后为11111010,再+1就是11111011,这就是它的补码了。
    >&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;下面的等式可以帮助理解补码在计算机中是如何工作的
    >&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; +5 + (-5) = 00000101 + 11111011 = 1 00000000 (溢出了!!!) = 0 
    >&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;这里的加法没有将数值位和符号位分开,而是统一作为二进制位进行计算,由于表示的是8进制的整数,所以多出的那个最高位的1会直接舍去,使得结果变成了0,而实际的十进制计算结果也是0,正确。
    >补码复习完毕,那么来看下下面这个表达式的含义:__<code>           x & (-x)       (其中 x >= 0)</code>
    >首先进行&运算,我们需要将x和-x都转化成补码,然后再看&之后会发生什么,我们假设 x 的二进制表示的末尾是连续的 k 个 0,令x的二进制表示为  X0X1X2…Xn-2Xn-1,  则 {Xi = 0 | n-k <= i < n}, 这里的X0表示符号位。
    >x的补码就是由三部分组成:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;(0)(X1X2…Xn-k-1)(k个0)   其中Xn-k-1为1,因为末尾是k个0,如果它为0,那就变成k+1个0了。
    >-x的补码也是由三部分组成:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;(1)(Y1Y2…Yn-k-1)(k个0)   其中Yn-k-1为1,其它的Xi和Yi相加为1,想想补码是怎么计算的就明白了。
    >那么 x & (-x) 也就显而易见了,由两部分组成 (1)(k个0),表示成十进制为 2k 啦。
    >由于&的优先级低于-,所以代码可以这样写:
    >```
    int lowbit(int x)
    {
        return x & -x;
    }
    >```
    
    ##小结:
    >__树状数组下标要从1开始,因为在修改值的时候,树状数组对于下标0不能正确处理<code> i += i & -i</code>得出来的结果一直是0,造成死循环__
    >对于维护的序列A,定义C[i]=A[j+1]+...+A[i],其中j为i的二进制表示中把最右边的1换成0的值。j的值可以通过lowbit求出,即i-lowbit(i)。
    lowbit(a)为2^(a的二进制表示末尾0的个数)。可以用下面式子求出
    >```
    lowbit(a)=a&(~a+1)
    >```
    或者根据补码的性质简化为
    >```
    lowbit(a)=a&(-a)
    >```
    修改方式如下
    >```
        void modify(int p,int delta)
        {
            while (p<=N)
            {
                C[p]+=delta;
                p+=lowbit(p);
            }
        }
    >```
    求前缀和如下
    >```
        int sum(int p)
        {
            int rs=0;
            while (p)
            {
                rs+=C[p];
                p-=lowbit(p);
            }
            return rs;
        }
    >```
    
    
    
    [1]:https://www.byvoid.com/blog/binary-index-tree
    [2]:http://www.cppblog.com/menjitianya/archive/2015/11/02/212171.html
  • 相关阅读:
    低耦合高内聚
    Python 爬虫库
    Python 爬虫的工具列表
    selenium对浏览器操作、鼠标操作等总结
    简单文件操作
    环境错误2
    环境错误
    pip list 警告消除方法
    python 安装scrapy错误
    按是否执行程序的角度划分:静态测试、动态测试
  • 原文地址:https://www.cnblogs.com/ZhaoxiCheung/p/5774710.html
Copyright © 2020-2023  润新知