BIT二叉索引树(树状数组)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/a1323933782/article/details/53698473

本文介绍BIT二叉索引树这种数据结构的搭建和应用。该数据结构能在动态修改的数组连续和查询问题上有极其出色的表现。

POWERED BY PHANTOM_LSH
本文知识和代码(c++)风格来源于刘汝佳的《算法竞赛入门经典 训练指南》


前缀和

现在有如下一个简单的问题:输入n个数据,储存在数组a[n]中,要求多次查询(sum(i , j))从i到j(即[i , j] )上所有元素的和。
由于查询较多,循环显然效率低下。可能大多数人都可以想到使用一个辅助数组s计算每个元素的前缀和(即s[x] = a[1]+a[2]+a[3]+......+a[x]),这样,当需要计算sum(i , j)时只需要计算s[j] - s[i-1]即可。如果不明白这一点请认真思考并将它理解。

动态连续和查询

    将上面的求连续和问题稍微改进一下,现在需要支持一种新的操作:add(x , d) 即把a[x]增加d。
    这样一来,如果通过前缀和的方式计算就不能简化计算了,因为每次修改一个元素都要修改所有在它后面的前缀和。有什么解决办法呢?我们需要用一种新的数据结构——BIT二叉索引树(树状数组)。

lowbit

    在介绍二叉索引树之前必须要介绍这样一个函数:lowbit(x),它返回的是正整数x的二进制表示方法中最靠右的一个1所代表的数字(例如:lowbit(10) = 2,因为10的二进制是1010,最靠右的一个1在第二位上,代表2)。
    其实,lowbit看似复杂其实代码实现异常简单。即:
int lowbit ( int x){return x&-x;}
    它的原理其实是:在计算机内部,负数(-x)是x按位取反(1变0,0变1)然后加1之后的结果(这里不讨论为什么),所以,-10的储存其实是0110 。然后再进行与运算,就可以得到lowbit了。

BIT

     终于进入了正题。运用lowbit我们可以把从1到n一段数字沿lowbit从大到小一半一半的分割开。比如:1-10中,8的lowbit最大,所以1-7一段,9-10一段,以此类推。所以,好像线段树那样,一段连续的数字被不停地分割直到只剩下一个数字。
     我们把lowbit相同的节点放在同一层(没有的补上去),这样自然形成一棵结点数为大于n的 最小的 2的某整数次方-1 的一棵完全二叉树,其根结点为lowbit最大的结点。通常我们把0也变成一个虚拟化的节点放在里面,方便运算。

一个简单的BIT示例
我们发现一个重要的结论:由于lowbit的定义,每个结点的两个子结点与该结点的二进制只有两位不同,一位是该节点的lowbit,两个子结点分别为0和1,另一位是子结点的lowbit,两个子结点都为1但是该结点为0 。这样的现象产生一个重要性质,即:对于一个结点x,它左上方(可以不是顺着边,而是首个向左上回溯的祖先结点)结点的编号为x-lowbit(x),而右上方(解释同上)结点的编号为x+lowbit(x) 。请打草稿或者仔细思考一下这个结论。
因此,我们可以不要储存这一棵树,因为边的关系可以直接计算。另:在BIT中只需要向上回溯。
一定要记住,BIT的结点储存的是原来的序列的下标!切记!!

动态连续和

    那么,BIT已经构造出来了,怎么用来解决问题呢?
    现在,构造辅助数组c[n](就像上面的s[n]一样),c[x] = a[x-lowbit(x)+1]+a[x-lowbit(x)+2]+a[x-lowbit(x)+3] + ……+a[x](a数组是原来的序列) 。这是什么意思呢?请看图:

BIT应用示例
每个结点都对应一个向左延伸的黄颜色的横条。横条所覆盖的数对应的序列值的和记录在c数组里面,就好像前缀和一样。
那么,c数组有什么用呢?
其实,如果要查询一个结点x的前缀和,只要不停地顺着x = x-lowbit(x) 迭代到0,将沿途的C数组的值全部相加就可以得到a[1]+a[2]+……+a[x]了。在上图中,就是顺着蓝色的箭头不停地向左上方移动,观察一下,是不是通过黄色的横条不重复不遗漏的覆盖了所有的下标?这就是查询操作了。
那么增加的操作呢?观察发现,只要沿着x = x+lowbit(x)不停地向右上方走(图上的红色箭头),直到走出边界,修改沿途的c值就可以了(使它们增加d)。因为能够覆盖到结点x的黄色横条只有那些。修改操作也就这样完成了!
最后再说一下预处理,预处理只要把a数组和c数组都清空,然后执行n次add操作,相当于从0开始add到原始数据就可以了!
看上去说的很多,其实代码实现很简单,下面附上c++版本的sum函数(前缀和)和add函数代码。

int sum(int x){ //求x前缀和
        int ans = 0;
        while(x>0){ //迭代到虚拟结点
            ans+=c[x];
            x -= lowbit(x); //向左上方移动
        }
        return ans;
    }
    void add(int x, int d){ //修改操作
        while(x<=n){ //判断是否超出边界
            c[x]+=d;
            x+=lowbit(x); //向右上方走
        }
        return ;
    }
最终,要求[a , b]的连续和只需要求sum(b) - sum(a-1)就可以了!

时间复杂度分析

    由于二叉树的基本特征,很容易看出修改和查询前缀和两个函数的时间复杂度都是O(logn),而预处理是n次add操作,所以预处理时间复杂度是O(nlogn) 在查询量多时远远优于暴力算法!

    好了,BIT二叉索引树(树状数组)就介绍到这里,我只是写下了我的理解,希望大家看了以后有所收获!刘汝佳的书上有一道例题,可以直接搜索LA 4329,供大家参考。
    谢谢阅读!

POWERED BY PHANTOM_LSH

猜你喜欢

转载自blog.csdn.net/a1323933782/article/details/53698473