• 树状数组-----入门级别


                        树状数组

          昨日初学树状数组,写一下自己的心得,如有不对之处,欢迎指出!!!

             对于树状数组,我现在的认知便是它可以用来解决区域间求和的问题,针对于那些,区间内元素可能改变的题有省时效果。比如有一串数存在a[1~n]中,要求区间i到j中元素的和,用s[i]数组保存前i项的总和,那么求区间和时间复杂度o(1),也就是s[j]-s[i-1] ,但是如果采取这种方法,那么一旦1~n中的某个值改动了,那么要更新该数后的所有s[],时间复杂度o(n)。(当然,如果不用s数组存也一样,就是查询时间变为o(n),而修改时间变为o(1)。)而如果采用树状数组保存,那么每次查询用时o(log n),每次更改也只需o(log n)的时间,总体来说,比较省时。

             树状数组指的便是下图中的c数组:(a[i]便是我们研究的数组,对它进行操作)

            

             由图可以看出:

    1. c[1]=a[1]
    2. c[2]=a[1]+a[2]
    3. c[3]=a[3]
    4. c[4]=a[1]+a[2]+a[3]+a[4]
    5. c[5]=a[5]
    6. c[6]=a[5]+a[6]
    7. c[7]=a[7]
    8. c[8]=a[1]+a[2]+ a[3]+a[4]+ a[5]+a[6]+ a[7]+a[8]

    对于单数i,c[i]便等于a[i],而对于偶数就要观察它的二进制下的数了。

    比如c[2]:二进制下的2为 10,可以说从右往左最多有一个连续的0,所以c[2]对应包括本身在内的左边2^1个元素,a[2]和a[1]。

    对于c[4]:二进制下的4为 100,从右往左最多有两个连续的0,所以c[4]对应包括自身在内的左边2^2个元素,a[4]、a[3]、a[2]、a[1]。

    同理,对于c[6],6二进制为110,有一个0,包括a[6]和a[5]两个元素。

    推广一下,单数也满足这个规律,比如c[5],5二进制为101,包括2^0个元素,即a[5]。

    在后面,我们用k来表示二进制下的该数末尾有几个0。

    (对于这个c数组的规律我们还可以看下图,加深印象)

     

    C数组构成一个满二叉树(a数组为叶子):对于k,我们还可以理解为这个c[i]子树的深度,而2^k则是叶子数。c[8]有两棵子树,c[4]和红框,两棵子树的叶子数自然是一样的,所以c[8]的叶子数是c[4]的两倍,c[8]的深度也比c[4]的大一。

    对于2^k,这个式子是非常经常用到的,因此对于这个式子的求法已经精简到我无法想象的地步,给出两个方法1.  2^k=i&(-i)  2.  2^k=i&(i^(i-1))。

    写成函数:

    int Lowbit(int x)

    {

      return x&(-x);

    }

    可以看一个例子,比如i=616:二进制为:0010  0110  1000 ,k为3,2^3即为8。

    (-i)为:

    (反码)1101  1001  0111

    (补码)1101  1001  1000

             i&(-i)为:

                       0000  0000  1000  即十进制下的8。

             其实可以发现,要求2^k,就是i二进制下的最右边的一个1和它右边所有的0。

             也就是说对于0010  0110  1000,我们要做的就是将1000单独取出,其它位都归0。

             如此,问题就好理解了一些,(-i)的反码首先把所有的二进制位全变得与i相反,所以i中的答案1000就变成0111,补码再加上1,自然又变回了1000,而1左边的数还是处于相反的状态,相与一下就把答案单独拿出了。

             说了这么多,我们回归到我们的目的,也就是求s数组身上,c数组我们可以看做是一个纽带,它将底层的a数组和目的s数组连在一起,以后对a数组的修改和对s数组的查询便集中到了c数组上。

             可以看出:

    1. s[1]=c[1]
    2. s[2]=c[2]
    3. s[3]=c[3]+c[2]
    4. s[4]=c[4]
    5. s[5]=c[5]+c[4]
    6. s[6]=c[6]+c[4]
    7. s[7]=c[7]+c[6]+c[4]
    8. s[8]=c[8]

    那么总结一下:

        对于s[3],先看c[3],c[3]有一个叶子a[3],那么求s[2],看c[2],c[2]有两个叶子a[1]+a[2],那么求s[0]=0,结束,s[3]=c[3]+c[2]。

             对于s[7],先看c[7],c[7]有1个叶子a[7],那么求s[6],看c[6],c[6]有两个叶子a[5]+a[6],那么求s[4],看c[4],c[4]有四个叶子a[1]+a[2]+a[3]+a[4],那么求s[0]=0,结束,s[7]=c[7]+c[6]+c[4]。

             写成函数如下:

    int  getsum(int  pos)

    {

                      int  sum=0;

                    while(pos>0)

                     {

                       sum+=c[pos];

                      pos-=Lowbit(pos);               //c[pos]包括了Lowbit(pos)个叶子,也就是说可以接着算s[pos- Lowbit(pos)]了。

                     }

             return sum;

    }

    这便是查询的时候需要做的事了。

    那么,除了查询我们还要应付的便是修改区间内的某个值,我们修改了a[i],那么就将有一系列的c[]需要修改,而这些要修改的c[]都是些什么呢,我们可以看出,经过修改遭受牵连的c数组,首先是直接联系的c[i],如图,c[i]都在a[i]的正上方,直接受到a[i]的影响,然后便是与c[i]有关的c[…]了,这些c[…]便是c[i]的祖先,比如c[2]改了,同时影响到c[4],c[8],c[16]……

     

    举例了:

    如果改了a[5],那么c[5]得改,c[5]有一个叶子,那么它的双亲即c[5+1]得改,c[6]有两个叶子,那么它的双亲c[6+2]得改。(双亲的计算原理便是前面讲过的左右子树叶子相同)

    写成函数如下:

    void update(int pos,int num,int n)                     //n—最大下标,num为改变量

    {

       while(pos<=n)

       {

         c[pos]+=num;

         pos+=Lowbit(pos);                   //c[pos]的叶子数即为Lowbit(pos)。

       }

    }

    这便是修改的时候要做的事了。

    那么,通过树状数组,我们便把对两个不同的数组a和s要进行的操作集中到了c数组上。

    下面我们来看一道题,地址:http://acm.hdu.edu.cn/showproblem.php?pid=4046

    大概题意就是说:给你一字符串,仅有b和w组成,而每次可能进行的操作有两种:

             0:输入i和j,输出i、j之间有多少”wbw”存在。

             1:输入i和一个字符c,表示将第i位改为c。

             大致思路:

    可以假想有一个a数组存放着以i为中心的三个字符是否满足条件,如果是就为1,不是就为0,而c数组便是一个树状数组记录a中的内容。对于每次修改,我们简化为:本次修改是否造成wbw数量减少(原有的被破坏),又是否造成它数量的增多(创造出新的wbw)。值得注意的是,查询i到j之间的wbw数量时,要转化成查询i+1和j-1之间的,因为边缘的值不满足条件。

    AC代码:

     1 #include<stdio.h>
     2 char s[50010];
     3 int c[50010];
     4 int Lowbit(int x)
     5 {
     6   return x&(-x);
     7 }
     8 void update(int pos,int num,int n)
     9 {
    10   while(pos<=n)
    11   {
    12     c[pos]+=num;
    13     pos+=Lowbit(pos);
    14   }
    15 }
    16 int getsum(int pos)
    17 {
    18   int sum=0;
    19   while(pos>0)
    20   {
    21     sum+=c[pos];
    22     pos-=Lowbit(pos);
    23   }
    24   return sum;
    25 }
    26 int main()
    27 {
    28   int i,j,g,t,n,m,k,h2,h1,x1,x2,p;
    29   char ch;
    30   while(scanf("%d",&t)!=EOF)
    31   {
    32     g=1;
    33     while(t--)
    34     {
    35       scanf("%d%d",&n,&m);
    36       scanf("%s",s+1);
    37       for(i=0;i<=n;i++)
    38         c[i]=0;
    39       for(i=2;i<=n;i++)
    40         if(s[i]=='b')
    41         {
    42           if((s[i-1]==s[i+1])&&s[i+1]=='w')
    43             update(i,1,n);
    44         }
    45       printf("Case %d:
    ",g++);
    46       for(i=0;i<m;i++)
    47       {
    48         scanf("%d",&p);
    49         if(p==0)
    50         {
    51           scanf("%d%d",&x1,&x2);
    52           if(x2-x1<2)
    53           {
    54             printf("0
    ");
    55             continue;
    56           }
    57           h2=getsum(x2);
    58           h1=getsum(x1+1);
    59           printf("%d
    ",h2-h1);
    60         }
    61         else if(p==1)
    62         {
    63           scanf("%d %c",&x1,&ch);
    64           x1++;
    65           if(s[x1]==ch) continue;
    66           for(j=x1-1;j<x1+2;j++)
    67           {
    68             if(j>1&&j<n&&s[j]=='b'&&(s[j-1]==s[j+1])&&s[j+1]=='w')
    69               update(j,-1,n);
    70           }
    71           s[x1]=ch;
    72           for(j=x1-1;j<x1+2;j++)
    73           {
    74             if(j>1&&j<n&&s[j]=='b'&&(s[j-1]==s[j+1])&&s[j+1]=='w')
    75               update(j,1,n);
    76           }
    77         }
    78       }
    79     }
    80   }
    81   return 0;
    82 }
  • 相关阅读:
    button标签和input button
    获取select标签的值
    window.loaction和window.location.herf
    数组重复计数,对象方法
    js对象详解
    面试经典题型整理
    一些js小知识点整理
    事件委托能够优化js性能
    网页加载的一般顺序
    http状态码
  • 原文地址:https://www.cnblogs.com/hchlqlz-oj-mrj/p/4501790.html
Copyright © 2020-2023  润新知