zoukankan      html  css  js  c++  java
  • 数据结构1——树状数组

    一、相关定义

    树状数组

    •  获取数组中连续n个数的和
    • 修改数组中某点的值
    • 时间复杂度:O(logn)

    小结:树状数组的强项在于对数组进行维护查询(如,修改某点的值、求某个区间的和)。当然,数据规模不大的时候,对于修改某点的值是非常容易的,复杂度是O(1),但是对于求一个区间的和就要扫一遍了,复杂度是O(N),如果实时的对数组进行M次修改或求和,最坏的情况下复杂度是O(M*N),当规模增大后这是划不来的!而树状数组干同样的事复杂度却是O(M*logN)。

    二、算法描述

    列出即将出现的知识定义:

    • lowbit(k):把k的二进制的高位1全部清空,只留下最低位的1(即只能剩下一个相对位数最低的1)
    • 求解lowbit(k):lowbit(k)=k&-k
    • lowbit(k)的作用:联系数组a和数组c
    • ck:从ak开始(包括ak往左连续求lowbit(k)个数
    • 去尾:把尾部应该去掉的1都去掉转而换到更高位的1,记住每次变换都要有一个高位的1产生
    • 去尾实现:k += lowbit(k)

    配图如下(左边为图A,右边为图B):

    【建立关系】

      图A是不是很像一颗树?对,这就是为什么叫树状数组了。A图中,a数组就是我们要维护和查询的数组,但是其实我们整个过程中根本用不到a数组,你可以把它当作一个摆设!c数组才是我们全程关心和操纵的重心。先由图来看看c数组的规则,其中c8=c4+c6+c7+a8,c6=c5+a6……先不必纠结怎么做到的,我们只要知道c数组的大致规则即可,很容易知道c8表示a1~a8的和,但是c6却是表示a5~a6的和,为什么会产生这样的区别的呢?或者说发明它的人为什么这样区别对待呢?答案是,这样会使操作更简单!看到这相信有些人就有些感觉了,为什么复杂度被log了呢?可以看到,c8可以看作a1~a8的左半边和+右半边和,而其中左半边和是确定的c4右半边其实也是同样的规则把a5~a8一分为二……继续下去都是一分为二直到不能分,可以看看B图。怎么样?是不是有点二分的味道了?对,说白了树状数组就是巧妙的利用了二分,它并不神秘,关键是它的巧妙!

           它又是怎样做到不断的一分为二呢?说这个之前我先说个叫lowbit的东西,lowbit(k)就是把k的二进制的高位1全部清空,只留下最低位的1,比如10的二进制是1010,则lowbit(10)=lowbit(1010)=(0010)2 。介于这个lowbit在下面会经常用到,这里给一个非常方便的实现方式,比较普遍的方法lowbit(k)=k&-k,这是位运算,我们知道一个数加一个负号是把这个数的二进制取反+1,如-10的二进制就是-1010=0101+1=0110,然后用1010&0110,答案就是0010了!明白了求解lowbit的方法就可以了,继续下面。介于下面讨论十进制已经没有意义(这个世界本来就是二进制的,人非要主观的构建一个十进制),下面所有的数没有特别说明都当作二进制。

           上面那么多文字说lowbit,还没说它的用处呢,它就是为了联系a数组和c数组的!ck表示从ak开始往左连续求lowbit(k)个数的和。比如c[0110]=a[0110]+a[0101],就是从0110开始(包括0110)往左计算了lowbit(0110)个数的和,(因为lowbit(0110)=0010)即从0110开始(包括0110)往左计算了0010(也就是2)个数的和,这0010个数分别为a[0110]与a[0101]。因为lowbit(0110)=0010,可以看到其实只有低位的1起作用,因为很显然可以写出c[0010]=a[0010]+a[0001],这就为什么我们任何数都只关心它的lowbit,因为高位不起作用(基于我们的二分规则它必须如此!),除非除了高位其余位都是0,这时本身就是lowbit。

    【更新位置数据】

      既然关系建立好了,看看如何实现a某一个位置数据更改的,它不会直接改的(开始就说了,a根本不存在),它每次改其实都要维护c数组应有的性质,因为后面求和要用到。而维护也很简单,比如更改了a[0011],我们接着要修改c[0011],c[0100],c[1000],这是很容易从图上看出来的,但是你可能会问,他们之间有什么必然联系吗?每次求解总不能总要拿图来看吧?其实从0011——>0100——>1000的变化都是进行“去尾”操作,就是把尾部应该去掉的1都去掉、转而换到更高位的1,记住每次变换都要有一个高位的1产生,所以0100是不能变换到0101的,因为没有新的高位1产生,这个变换过程恰好是可以借助我们的lowbit进行的,k += lowbit(k)

           好吧,现在更新的次序都有了,可能又会产生新的疑问了:为什么它非要是这种关系啊?这就要追究到之前我们说c8可以看作a1~a8的左半边和+右半边和……的内容了,为什么c[0011]会影响到c[0100]而不会影响到c[0101],这就是之前说的c[0100]的求解实际上是这样分段的区间 c[0001]~c[0001] 和区间c[0011]~c[0011]的和,数字太小,可能这样不太理解,在比如c[0100]会影响c[1000],为什么呢?因为c[1000]可以看作0001~0100的和加上0101~1000的和,但是0101位置的数变化并会直接作用于c[1000],因为它的尾部1不能一下在跳两级在产生两次高位1,是通过c[0110]间接影响的,但是,c[0100]却可以跳一级产生一次高位1

             可能上面说的你比较绕了,那么此时你只需注意:c的构成性质(其实是分组性质)决定了c[0011]只会直接影响c[0100],而c[0100]只会直接影响[1000],而下表之间的关系恰好是也必须是k +=lowbit(k)。

    此时我们就是写出更新维护树的代码:

    void add(int k,int num)
    {
    	while(k<=n)
    	{
    		tree[k]+=num;
    		k += k&-k;    //k&-k 就是lowbit(k)
    	}
    }
    

    有了上面的基础,说求和就比较简单了。

    比如求0001~0110的和就直接c[0100]+c[0110],分析方法与上面的恰好逆过来,而且写法也是逆过来的,具体就不赘述了:

    int read(int k)  //1~k的区间和
    {
    	int sum=0;
    	while(k)
    	{
    		sum += tree[k];
    		k -= k&-k;
    	}
    	return sum;
    }
    

    三、博客小结

    1. 树状数组说白了是按照二分对数组进行分组;
    2. 维护和查询都是O(logn)的复杂度;
    3. lowbit这里只是一个技巧,关键在于明白c数组的构成规律
    4. 分析的过程二进制一定要深入人心,当作心目中的十进制
  • 相关阅读:
    PAT (Advanced Level) 1114. Family Property (25)
    PAT (Advanced Level) 1113. Integer Set Partition (25)
    PAT (Advanced Level) 1112. Stucked Keyboard (20)
    PAT (Advanced Level) 1111. Online Map (30)
    PAT (Advanced Level) 1110. Complete Binary Tree (25)
    PAT (Advanced Level) 1109. Group Photo (25)
    PAT (Advanced Level) 1108. Finding Average (20)
    PAT (Advanced Level) 1107. Social Clusters (30)
    PAT (Advanced Level) 1106. Lowest Price in Supply Chain (25)
    PAT (Advanced Level) 1105. Spiral Matrix (25)
  • 原文地址:https://www.cnblogs.com/xzxl/p/7243543.html
Copyright © 2011-2022 走看看