美文网首页线段树
从一道水题来从头介绍树状数组

从一道水题来从头介绍树状数组

作者: 碧影江白 | 来源:发表于2016-08-26 20:45 被阅读244次

    树状数组用来求区间元素和,求一次区间元素和的时间效率为O(logn)。特别用于在数组内的参数变换后,再次求和所使用的时间复杂度减少问题。
    下面是一道水题:POJ上的stars,题目大意为:按照纵坐标y从小到大的顺序依次输入几个星星的横纵坐标,横纵坐标小于等于该星星的星星数量即为该星星的等级,求每个等级的星星数量。
    由于输入的顺序是按照纵坐标从小到大的顺序(y相同时x的坐标也是由小到大输出),那么一个星星的满足条件的星星一定在输入的这个星星的前方,故在这道题中,y坐标是没有用处的,我们可以每次输入一个x值,就判断之前输入过的数中,小于x的星星的个数,例如:输入坐标:
    5 1
    1 2
    2 2
    3 2
    那么第三个数据之前有第二和第三两个符合条件(横坐标是1,2都小于5),所以第三颗星星的等级是2,但是如果按照这种方法逐个统计的话,在星星的数量达到一定数量时,遍历一次并且每个都判断一次的话,是一定超时的,所以,可以想到,开辟一个数组ar,下标1:ar[1]代表的是在输入到当前时,横坐标为1的个数,ar[2]即是横坐标为2的星星的个数,依次类推,加入当前输入的横坐标是4,那么结果就是ar[0]+ar[1]+ar[2]+ar[3]+arr[4](纵坐标按顺序输入,那么则表明已经输出的星星纵坐标一定小于等于当前纵坐标,只要横坐标满足条件则整体一定满足),结果保留,并将ar[4]++。
    但是,如果当前输入的横坐标n达到了数万位,那么还要继续从ar[0]遍历求和到ar[n]吗?当然是行不通的,所以,又需要将该模型进行优化。
    我们的方法是:如果每间隔一段距离,一个arr数空间存放的就是前面的一段数组的和是不是会简便很多?那样就不需要每次都使用遍历所有的节点来求和了,至少会遍历较少的数,那么,这就涉及到了树状数组,不妨回忆一下二叉树的原型,最上面一个根结点,下面每个节点都展开分支,我们假设每个叶子节点代表ar[i],他们的根结点来记录和,那么整棵树的根节点就是和了,想想是不是方便了很多?


    如果当前横坐标是3,那么只要输出C的值,这样减少了很多遍历的时间,但是同时存在的问题是,如果当前横坐标是0的话,还可以输出ar[0],一的话输出A,那么如果2的话呢?输出的又该怎么处理呢?还有应该怎样在输出的时候顺利得找到A,或C点呢?树状数组的处理将会解决这些问题:
    下面是从网上转载的一些关于树状数组的详细推断,以二进制的形式方便的找到每一个范围内的非叶子节点:

    假设树状数组C[],序列为A[1]~A[8]
    网络上面都有这个图,但是我将这个图做了2点改进。


    (1)图中有一棵满二叉树,满二叉树的每一个结点对应A[]中的一个元素。
    (2)C[i]为A[i]对应的那一列的最高的节点。
    那么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”,那么拥有2x个叶子,为序列A[]中的第i-2x+1到第i个元素的和。

    终于,我们得到了一个C[i]的具体定义:
    C[i]=A[i-2^x+1]+…+A[i],其中x为i的二进制中的从右往左数有连续“0”的个数。

    利用树状数组求前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]。

    也就是说,每个C[i]所存放的是2^x个数据的和,或者说,每个C[i]内和的下标在 i~i-2x+1,下一个C[i-1]的第一个数为i-2x,C[i-1]=C[i-2x],将i=i-2x,不断递归,结果则为总的和。

    讲到这里其实有点难度,因为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;
    }
    

    更新C[]
    当有星星不断输入,那么A的数目也会随之改变,所以,如果数组A[i]被更新了怎么办?那么如何改动C[]?

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

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


    如上图:
    假如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”的左子树和右子树的叶子数是相同的。 因为2x代表的就是叶子数,所以C[3]的父亲是A,A的父亲是C[i+20],即C[3]改变,那么C[3+2^0]也改变。

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

    推广到一般情况就是:
    如果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);
           }
    }
    

    经过以上推断,我们便可以据此得到该题的具体代码实现了:

    #include <iostream>
    #include <algorithm>
    #include <cstdio>
    #include <math.h>
    #include <string.h>
    #define lowbit(x)  (x&(-x))
    #define MMAX 15005
    #define NMAX 32005
    using namespace std;
    
    int C[NMAX];
    
    int sum(int i)
    {
        int ans=0;
        while (i>0)
        {
            ans+=C[i];
            i-=lowbit(i);
        }
    }
    
    void add(int num,int i)
    {
        while (i<=NMAX)
        {
            C[i]+=num;
            i+=lowbit(i);
        }
    }
    
    
    void main()
    {
        int arr[MMAX]={0};
        int i,j,k,n,m,x,y;
        scanf("%d",&n);
        m=n;
        while (n--)
        {
            scanf("%d%d",&x,&y);
            arr[sum(++x)]++;
            add(1,x);
        }
        for (i=0;i<=m;i++)
            printf("%d\n",arr[i]);
    }
    

    以上是一维树状数组的详解,关于二维的树状数组,则与一维的相似,主要在于元素数组为一个二维的A[i][j],相对应的是,树状数组同样对应的一个二维数组C[i][j],不同的是C[i][j]代表的是0到i行和0到j列的和。
    下面是在网络上找到的相关注解:

    一维树状数组很容易扩展到二维,在二维情况下:
    数组A[][]的树状数组定义为:
    C[x][y] = ∑ a[i][j], 其中,
    x-lowbit(x) + 1 <= i <= x,
    y-lowbit(y) + 1 <= j <= y.
    例:举个例子来看看C[][]的组成。
    设原始二维数组为:A[][]={{a11,a12,a13,a14,a15,a16,a17,a18,a19},
    {a21,a22,a23,a24,a25,a26,a27,a28,a29},
    {a31,a32,a33,a34,a35,a36,a37,a38,a39},
    {a41,a42,a43,a44,a45,a46,a47,a48,a49}};
    那么它对应的二维树状数组C[][]呢?
    记: B[1]={a11,a11+a12,a13,a11+a12+a13+a14,a15,a15+a16,...}
    这是第一行的一维树状数组
    B[2]={a21,a21+a22,a23,a21+a22+a23+a24,a25,a25+a26,...}
    这是第二行的一维树状数组
    B[3]={a31,a31+a32,a33,a31+a32+a33+a34,a35,a35+a36,...}
    这是第三行的一维树状数组
    B[4]={a41,a41+a42,a43,a41+a42+a43+a44,a45,a45+a46,...}
    这是第四行的一维树状数组
    那么:
    C[1][1]=a11,C[1][2]=a11+a12,C[1][3]=a13,C[1][4]=a11+a12+a13+a14,c[1][5]=a15,C[1][6]=a15+a1
    6,...
    这是A[][]第一行的一维树状数组
    C[2][1]=a11+a21,C[2][2]=a11+a12+a21+a22,C[2][3]=a13+a23,C[2][4]=a11+a12+a13+a14+a21+a2
    2+a23+a24,
    C[2][5]=a15+a25,C[2][6]=a15+a16+a25+a26,...
    这是A[][]数组第一行与第二行相加后的树状数组
    C[3][1]=a31,C[3][2]=a31+a32,C[3][3]=a33,C[3][4]=a31+a32+a33+a34,C[3][5]=a35,C[3][6]=a35+a3 6,...
    这是A[][]第三行的一维树状数组
    C[4][1]=a11+a21+a31+a41,C[4][2]=a11+a12+a21+a22+a31+a32+a41+a42,C[4][3]=a13+a23+a33+a43,...
    这是A[][]数组第一行+第二行+第三行+第四行后的树状数组
    搞清楚了二维树状数组C[][]的规律了吗?
    仔细研究一下,会发现:

    (1)在二维情况下,如果修改了A[i][j]=delta,
    则对应的二维树状数组更新函数为:
    private void Modify(int i, int j, int delta){
    A[i][j]+=delta;
    for(int x = i; x< A.length; x += lowbit(x))
    for(int y = j; y <A[i].length; y += lowbit(y)){
    C[x][y] += delta; } }

    (2)在二维情况下,求子矩阵元素之和∑ a[i]j的函数为
    int Sum(int i, int j){
    int result = 0;
    for(int x = i; x > 0; x -= lowbit(x)) {
    for(int y = j; y > 0; y -= lowbit(y)) {
    result += C[x][y]; } }
    return result; }
    比如:
    Sun(1,1)=C[1][1]; Sun(1,2)=C[1][2]; Sun(1,3)=C[1][3]+C[1][2];...
    Sun(2,1)=C[2][1]; Sun(2,2)=C[2][2]; Sun(2,3)=C[2][3]+C[2][2];...
    Sun(3,1)=C[3][1]+C[2][1]; Sun(3,2)=C[3][2]+C[2][2];

    相关文章

      网友评论

        本文标题:从一道水题来从头介绍树状数组

        本文链接:https://www.haomeiwen.com/subject/wyjzsttx.html