zoukankan      html  css  js  c++  java
  • 学习笔记:树状数组

    一、树状数组是干什么的?

           平常我们会遇到一些对数组进行维护查询的操作,比较常见的如,修改某点的值、求某个区间的和,而这两种恰恰是树状数组的强项!当然,数据规模不大的时候, 对于修改某点的值是非常容易的,复杂度是O(1),但是对于求一个区间的和就要扫一遍了,复杂度是O(N),如果实时的对数组进行M次修改或求和,最坏的 情况下复杂度是O(M*N),当规模增大后这是划不来的!而树状数组干同样的事复杂度却是O(M*lgN),别小看这个lg,很大的数一lg就很小了,这 个学过数学的都知道吧,不需要我说了。申明一下,看下面的文章一定不要急,只需要看懂每一步最后自然就懂了。

    二、树状数组怎么干的?

            先看两幅图(网上找的,如果雷同,不要大惊小怪~),下面的说明都是基于这两幅图的,左边的叫A图吧,右边的叫B图:

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

           她又是怎样做到不断的一分为二呢?说这个之前我先说个叫lowbit的东西,lowbit(k)就是把k的二进制的高位1全部清空,只留下最低位的1,比 如10的二进制是1010,则lowbit(k)=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],就是从110开始计算了0010个数的和,因为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)。此时我们就是写出跟新维护树的代码:

    1     void add(int k,int num)  
    2     {  
    3         while(k<=n)  
    4         {  
    5             tree[k]+=num;  
    6             k+=k&-k;  
    7         }  
    8     }  

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

     1     int read(int k)//1~k的区间和  
     2     {  
     3         int sum=0;  
     4         while(k)  
     5         {  
     6             sum+=tree[k];  
     7             k-=k&-k;  
     8         }  
     9         return sum;  
    10     }  

    三、总结一下吧

              首先,明白树状数组所白了是按照二分对数组进行分组;维护和查询都是O(lgn)的复杂度,复杂度取决于最坏的情况,也是O(lgn);lowbit这里 只是一个技巧,关键在于明白c数组的构成规律;分析的过程二进制一定要深入人心,当作心目中的十进制。

    (参考:http://blog.csdn.net/int64ago/article/details/7429868

     【模板】{代码}

      1 #include<iostream>
      2 #include<cstdio>
      3 #include<cstring>
      4 #include<cmath>
      5 #include<algorithm>
      6 #include<queue>
      7 #include<cstdlib>
      8 #include<iomanip>
      9 #include<cassert>
     10 #include<climits>
     11 #define maxn 100001
     12 #define F(i,j,k) for(int i=j;i<=k;i++)
     13 #define M(a,b) memset(a,b,sizeof(a))
     14 #define FF(i,j,k) for(int i=j;i>=k;i--)
     15 #define inf 0x7fffffff
     16 #define maxm 21
     17 //#define LOCAL
     18 using namespace std;
     19 int read(){
     20     int x=0,f=1;char ch=getchar();
     21     while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
     22     while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
     23     return x*f;
     24 }
     25 int n,m;
     26 int a[maxn],c[maxn];
     27 int last[maxn];
     28 inline int lowbit(int x)
     29 {
     30     return x&-x;
     31 }
     32 inline void add(int k,int num)
     33 {//建树&增加常数 
     34     last[k]+=num;
     35     while(k<=n)
     36     {
     37         c[k]+=num;
     38         k+=lowbit(k);
     39     }
     40 //    F(i,1,n)cout<<c[i]<<" ";cout<<endl;
     41     return;
     42 }
     43 inline void add_interval(int l,int r,int k)
     44 {//区间增减【慢】(nlogn) 
     45     F(i,l,k){
     46         last[i]+=k;
     47     }
     48     F(i,l,r){
     49         add(i,k);
     50     }
     51 //    F(i,1,n)cout<<c[i]<<" ";cout<<endl;
     52     return;
     53 }
     54 inline void change(int x,int y)
     55 {//单点修改 
     56     int aa,bb;
     57     aa=last[x];//cout<<aa<<endl;
     58     y=y-aa;//cout<<y<<endl;
     59     add(x,y);//cout<<y<<endl;
     60 //    F(i,1,n)cout<<c[i]<<" ";cout<<endl;
     61     return;
     62 }
     63 inline int query_pro(int k)
     64 {//求前缀和
     65     int sum=0;
     66     while(k)
     67     {
     68         sum+=c[k];
     69         k-=lowbit(k);
     70     }
     71     return sum;
     72 }
     73 inline int query_single(int k)
     74 {//单点查询 
     75     int sum=0;
     76     sum=query_pro(k)-query_pro(k-1);
     77     return sum;
     78 }
     79 inline int query_sum(int l,int r)
     80 {//区间求和
     81     int sum=0;
     82     sum=query_pro(r)-query_pro(l-1);
     83     return sum; 
     84 }
     85 int main()
     86 {
     87     std::ios::sync_with_stdio(false);//cout<<setiosflags(ios::fixed)<<setprecision(1)<<y;
     88     #ifdef LOCAL 
     89     freopen("data.in","r",stdin);
     90     freopen("data.out","w",stdout);
     91     #endif
     92     cin>>n;
     93     F(i,1,n){
     94         cin>>a[i];
     95         add(i,a[i]);
     96     }
     97     /*
     98     int opt,x,y,z;
     99     while(1)
    100     {
    101         cin>>opt;
    102         switch(opt)
    103         {
    104             case 1:cin>>x>>y;add(x,y);break;
    105             case 2:cin>>x>>y>>z;add_interval(x,y,z);break;
    106             case 3:cin>>x>>y;change(x,y);break;
    107             case 4:cin>>x;cout<<query_single(x)<<endl;break;
    108             case 5:cin>>x;cout<<query_pro(x)<<endl;break;
    109             case 6:cin>>x>>y;cout<<query_sum(x,y)<<endl;break;
    110         }
    111     } */
    112     return 0;
    113 }
    View Code
  • 相关阅读:
    Django对静态文件的处理——部署阶段
    使用Django来处理对于静态文件的请求
    Django1.7如何配置静态资源访问
    Spring WebSocket中403错误解决
    FastJSON JSONObject 字段排序 Feature.OrderedField
    国际化(i18n) 各国语言缩写
    【转】java.io.Closeable接口
    【转】spring bean 卸载
    This content should also be served over HTTPS
    Failed to close the ServletOutputStream connection cleanly, Broken pipe
  • 原文地址:https://www.cnblogs.com/SBSOI/p/5647184.html
Copyright © 2011-2022 走看看