树状数组深度剖析

前言


树状数组最开始学的时候因为整不明白,所以就只是知道怎么用,网上很多人对着结论口胡树状数组的原理,印象不是特别深刻。后来重新学习了一遍,看了下知乎关于树状数组的讨论,希望能深入了解树状数组的原理,但是真的有点难理解,而且说法和思想有好几种,有的地方有可能还不够严谨,希望看完能对你有所启发

引入


假设我们有个一维数组,要进行两种操作:

1.修改某个元素

2.求指定区间的和

对于这个问题,我们有两种简单的解法

  • 修改元素$i$直接通过下标访问($O(1)$),求区间和就按照区间进行累加($O(n)$)
  • 先进行前缀和预处理,$sum[r]-sum[l-1]$得到区间和($O(1)$),但是修改元素$i$,就要把$i$之前的前缀和都更新($O(n)$)

可以看出两种方法各有优劣,一类折中的思路是同样存储一些区间内的计算结果,节省求和的时间,但尽量减少重叠区间的数量,从而减少更新的次数。

原理


 如何高效的统计和修改数组内的信息呢?我们可以想到用树形结构

如果只需要查询前缀和的话,在线段树上查询是不需要用到右儿子的值的(如果要用,那么左儿子的值会被用到,那么还不如用父亲节点的值),所以去掉所有右儿子,就得到了树状数组的结构,如图:

 而我们可以把这里的$lowbit$理解为树的高度。观察下可以发现对于位置 $i$,其对应的结点所在的高度就是 $lowbit(i) $的位数,第一层是所有$lowbit(i)=1$的节点,第二层是所有$lowbit(i)=2$的节点······

节点的高度又决定了其子树的大小,因此对于节点$i$,它所维护的信息区间为$(i-lowbit(i),i]$

对于区间查询,我们是用右边界前缀和减去左边界前缀和得到。

由于任何正整数都能表示为2的幂相加的形式,那么求前缀和可以由多个长度为2的幂的区间的和得到。比如$19=2^{4}+2^{1}+2^{0}$,就是由前16个元素的和,再往后两个元素的和,再往后一个元素相加得到。那么我们可以不断去掉二进制末尾的1,统计对应区间的信息进行相加就可以了。

至于单点修改,理解起来可能稍微难懂一点(个人观点)

由于树形结构维护区间信息,那从这个节点往父节点走一直走到根节点,路径上的节点都要修改。关键在于如何找到父节点,其实对于当前节点$i$,它下一个存在的父节点就是$i+lowbit(i)$,为什么呢?

留坑吧,发现之前的证明存在问题

 实现


了解了树状数组的原理后,想必实现起来思路就会清晰很多

int lowbit(int x){
    return x&(-x);
} 
//区间求和 
int sum(int x){
    int ret = 0
    while(i){
        ret += c[x];
        x -= lowbit(x); 
    }
    return ret;
} 
//单点修改 
void update(int x, int val){
    while(x<=n){
        c[x] += val;
        x += lowbit(x); //向上更新 
    }
} 

这里解释下$lowbit(i)$为什么可以用$i\&(-i)$表示,计算机中的负数用对应正数的补码(正数取反加一)来表示

假设这里的$i$我表示成xxxxx100,那么$-i$就是#####011+1 = #####100(这里的#表示对x取反)

那么(xxxxx100)&(#####100) = 00000100,就是$i$的二进制的最低位1对应的值

 


Reference:

https://www.zhihu.com/question/54404092

https://www.cnblogs.com/acgoto/p/8583952.html

 

posted @ 2020-02-14 23:21  sparkyen  阅读(266)  评论(0编辑  收藏  举报