zoukankan      html  css  js  c++  java
  • 树状数组详解(转)

    第01讲 什么是树状数组?

    树状数组用来求区间元素和,求一次区间元素和的时间效率为O(logn)。

    有些同学会觉得很奇怪。用一个数组S[i]保存序列A[]的前i个元素和,那么求区间i,j的元素和不就为S[j]-S[i-1],那么时间效率为O(1),岂不是更快?

    但是,如果题目的A[]会改变呢?例如:

    我们来定义下列问题:我们有n个盒子。可能的操作为

    1.向盒子k添加石块

    2.查询从盒子i到盒子j总的石块数

    自然的解法带有对操作1为O(1)而对操作2为O(n)的时间复杂度。但是用树状数组,对操作1和2的时间复杂度都为O(logn)。

    第02讲 图解树状数组C[]

    现在来说明下树状数组是什么东西?假设序列为A[1]~A[8]

    [转载]树状数组

    网络上面都有这个图,但是我将这个图做了2点改进。

    (1)图中有一棵满二叉树,满二叉树的每一个结点对应A[]中的一个元素。

    (2)C[i]为A[i]对应的那一列的最高的节点。

    现在告诉你:序列C[]就是树状数组。

    那么C[]如何求得?

    C[1]=A[1];

    C[2]=A[1]+A[2];

    C[3]=A[3];

    C[4]=A[1]+A[2]+A[3]+A[4];

    C[5]=A[5];

    C[6]=A[5]+A[6];

    C[7]=A[7];

    C[8]= A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];

    以上只是枚举了所有的情况,那么推广到一般情况,得到一个C[i]的抽象定义:

    因为A[]中的每个元素对应满二叉树的每个叶子,所以我们干脆把A[]中的每个元素当成叶子,那么:C[i]=C[i]的所有叶子的和。

    现在不得不引出关于二进制的一个规律:

    先仔细看下图:

    [转载]树状数组

    将十进制化成二进制,然后观察这些二进制数最右边1的位置:

    1 --> 00000001

    2 --> 00000010

    3 --> 00000011

    4 --> 00000100

    5 --> 00000101

    6 --> 00000110

    7 --> 00000111

    8 --> 00001000

    1的位置其实从我画的满二叉树中就可以看出来。但是这与C[]有什么关系呢?

    接下来的这部分内容很重要:

    在满二叉树中,

    以1结尾的那些结点(C[1],C[3],C[5],C[7]),其叶子数有1个,所以这些结点C[i]代表区间范围为1的元素和;

    以10结尾的那些结点(C[2],C[6]),其叶子数为2个,所以这些结点C[i]代表区间范围为2的元素和;

    以100结尾的那些结点(C[4]),其叶子数为4个,所以这些结点C[i]代表区间范围为4的元素和;

    以1000结尾的那些结点(C[8]),其叶子数为8个,所以这些结点C[i]代表区间范围为8的元素和。

    扩展到一般情况:

    i的二进制中的从右往左数有连续的x个“0”,那么拥有2^x个叶子,为序列A[]中的第i-2^x+1到第i个元素的和。

    终于,我们得到了一个C[i]的具体定义:

    C[i]=A[i-2^x+1]+…+A[i],其中x为i的二进制中的从右往左数有连续“0”的个数。

    第03讲 利用树状数组求前i个元素的和S[i]

    理解了C[i]后,前i个元素的和S[i]就很容易实现。

    从C[i]的定义出发:

    C[i]=A[i-2^x+1]+…+A[i],其中x为i的二进制中的从右往左数有连续“0”的个数。

    我们可以知道:C[i]是肯定包括A[i]的,那么:

    S[i]=C[i]+C[i-2^x]+…

    也许上面这个公式太抽象了,因为有省略号,我们拿一个具体的实例来看:

    S[7]=C[7]+C[6]+C[4]

    因为C[7]=A[7],C[6]=A[6]+A[5],C[4]=A[4]+A[3]+A[2]+A[1],所以S[7]=C[7]+C[6]+C[4]

    (1)i=7,求得x=0,那么我们求得了A[7];

    (2)i=i-2^x=6,求得x=1,那么求得了A[6]+A[5];

    (3)i=i-2^x=4,求得x=2,那么求得了A[4]+A[3]+A[2]+A[1]。

    讲到这里其实有点难度,因为S[i]的求法,如果要讲清楚,那么得写太多的东西了。所以不理解的同学,再反复多看几遍。

    从(1)(2)(3)这3步可以知道,求S[i]就是一个累加的过程,如果将2^x求出来了,那么这个过程用C++实现就没什么难度。

    现在直接告诉你结论:2^x=i&(-i)

    证明:设A’为A的二进制反码,i的二进制表示成A1B,其中A不管,B为全0序列。那么-i=A’0B’+1。由于B为全0序列,那么B’就是全1序列,所以-i=A’1B,所以:

    i&(-i)= A1B& A’1B=1B,即2^x的值。

    所以根据(1)(2)(3)的过程我们可以写出如下的函数:

    int Sum(int i) //返回前i个元素和

    {

           int s=0;

           while(i>0)

           {

                  s+=C[i];

                  i-=i&(-i);

           }

           return s;

    }

    第04讲 更新C[]

    正如第01讲提到的小石块问题,如果数组A[i]被更新了怎么办?那么如何改动C[]?

    如果改动C[]也需要O(n)的时间复杂度,那么树状数组就没有任何优势。所以树状数组在改动C[]上面的时间效率为O(logn),为什么呢?

    因为改动A[i]只需要改动部分的C[]。这一点从第02讲的图中就可以看出来:

    [转载]树状数组

    如上图:

    假如A[3]=3,接着A[3]+=1,那么哪些C[]需要改变呢?

    答案从图中就可以得出:C[3],C[4],C[8]。因为这些值和A[3]是有联系的,他们用树的关系描述就是:C[3],C[4],C[8]是A[3]的祖先。

    那么怎么知道那些C[]需要变化呢?

    我们来看“A”这个结点。这个“A”结点非常的重要,因为他体现了一个关系:A的叶子数为C[3]的2倍。因为“A”的左子树和右子树的叶子数是相同的。 因为2^x代表的就是叶子数,所以C[3]的父亲是A,A的父亲是C[i+2^0],即C[3]改变,那么C[3+2^0]也改变。

    我们再来看看“B”这个结点。B结点的叶子数为2倍的C[6]的叶子数。所以B和C[6+2^1]在同一列,所以C[6]改变,C[6+2^1]也改变。

    推广到一般情况就是:

    如果A[i]发生改变,那么C[i]发生改变,C[i]的父亲C[i+2^x]也发生改变。

    这一行的迭代过程,我们可以写出当A[i]发生改变时,C[]的更新函数为:

    void Update(int i,int value)  //A[i]的改变值为value

    {

           while(i<=n)

           {

                  C[i]+=value;

                  i+=i&(-i);

           }

    }

    第05讲 一维树状数组的应用举例

    废了4讲的话,我们终于把一维树状数组的2个不到5行的代码给搞定了。现在要正式投入到应用当中。

    题目链接:http://poj.org/problem?id=2352

    题意:按照y升序给你n个星星的坐标,如果有m个星星的x,y坐标均小于等于星星A的坐标,那么星星A的等级为m。

    分析:是一道树状数组题。举例来说,以下是题目的输入:

    5

    1 1

    5 1

    7 1

    3 3

    5 5

    由于y坐标是升序的且坐标不重复,所以在星星A后面输入的星星的x,y坐标不可能都小于等于星星A。假如当前输入的星星为(3,3),易得我们只需要去找 树状数组中小于等于3的值就可以了,即GetSum(3)。注意:A[i]表示x坐标为i的个数,C[]为A[]的树状数组,那么GetSum(i)就是 序列中前i个元素的和,即x小于等于i的星星数。

    本题还是一点要注意:星星坐标的输入可以是(0,0),所以我们把x坐标统一加1,然后用树状数组实现。

    第06讲 二维树状数组

    BIT可用为二维数据结果。假设你有一个带有点的平面(有非负的坐标)。你有三个问题:

    1.在(x , y)设置点

    2.从(x , y)移除点

    3.在矩形(0 , 0), (x , y)计算点数 - 其中(0 , 0)为左下角,(x , y)为右上角,而边是平行于x轴和y轴。

    对于1操作,在(x,y)处设置点,即Update(x,y,1),那么这个Update要怎么写?很简单,因为x,y坐标是离散的,所以我们分别对x,y进行更新即可,函数如下:

    void Update(int x,int y,int val)

    {

           while(x<=n)

           {

                  int y1=y;

                  while(y1<=n)

                  {

                         C[x][y1]+=val;

                         y1+=y1&(-y1);

                  }

                  x+=x&(-x);

           }

    }

    那么根据Update可以推得:GetSum函数为:

    int GetSum(int x,int y)

    {

           int sum=0;

           while(x>0)

           {

                  int y1=y;

                  while(y1>0)

                  {

                         sum+=C[x][y1];

                         y1-=y1&(-y1);

                  }

                  x-=x&(-x);

           }

           return sum;

    }

    第07讲 二维树状数组的应用举例

    题目链接:http://poj.org/problem?id=2155

    我们先讨论POJ2155的一维情况,如下:

    有一个n卡片的阵列。每个卡片倒放在桌面上。你有两个问题:

      1. T i j (反转从索引i到索引j的卡片,包括第i张和第j张卡——面朝下的卡将朝上;面朝上的卡将朝下)

      2. Q i (如果第i张卡面朝下回答0否则回答1)

    解决:

    解决问题(1和2)的方法有时间复杂度O(log n)。在数组f(长度n + 1)我们存储每个问题T(i, j)——我们设置f[i]++和f[j + 1]--。对在i和j之间(包括i和j)每个卡k求和f[1] + f[2] + ... + f[k]将递增1,其他全部和前面的一样(看图2.0清楚一些),我们的结果将描述为和(和累积频率一样)模2。

    [转载]树状数组

    图 2.0

    使用BIT来存储(增加/减少)频率并读取累积频率。

    理解了一维的情况,POJ2155就是其二维的版本,易得只需要更(x1,y1),(x1,y2+1),(x2+1,y1),(x2+1,y2+1)四个点的C[]的值就可以了,最后的结果依然是GetSum(x,y)%2

    keep moving...
  • 相关阅读:
    数组与字符串的相互转换
    数组新增,修改json数据
    百度Ueditor设置图片自动压缩
    微信小程序——自定义图标组件
    微信小程序——自定义导航栏
    微信小程序——网盘图片预览
    微信小程序——星星评分
    微信小程序——页面中调用组件方法
    Vue路由获取路由参数
    C#随机颜色和随机字母
  • 原文地址:https://www.cnblogs.com/xxx0624/p/2612364.html
Copyright © 2011-2022 走看看