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

                    树状数组

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

         对于树状数组,我现在的认知便是它可以用来解决区域间求和的问题,针对于那些,区间内元素可能改变的题有省时效果。比如有一串数存在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:\n",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\n");
55             continue;
56           }
57           h2=getsum(x2);
58           h1=getsum(x1+1);
59           printf("%d\n",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 }

 

posted @ 2015-05-13 21:47  hchlqlz  阅读(572)  评论(5编辑  收藏  举报