树状数组

转载自:点击打开链接  感谢作者

代码部分前 的讲解主要 基于 线段树单点更新,区间查询  后面的为算法变形延伸

树状数组,又称二进制索引树,英文名Binary Indexed Tree。

一、树状数组的用途

主要用来求解数列的前缀和,a[0]+a[1]+...+a[n]。

由此引申出三类比较常见问题:

1、单点更新,区间求值。(HDU1166)

2、区间更新,单点求值。(HDU1556)

3、求逆序对。(HDU2838)

 

二、树状数组的表示

1、公式表示

设A[]为一个已知的数列。C[]为树状数组。则会有

C[i]=A[j]+...+A[i];而且有个有趣的性质,设节点编号为i,那么这个节点管辖的区间为2^k(其中k为x二进制末尾0的个数)个元素

具体算这个2^k有一个快捷的办法:x & (-x)  或者 x & (x ^ (x - 1) )

2、图形表示

(注:最下面的一行表示数组A,上面的二进制表示的部分是C)

从以上可以发现:

1、树状数组C是表示普通数组A的一部分的和。

2、小标为奇数时,C[i]只能管辖一个A[i]。

3、C[i]的最后一个数一定是A[i]。

树状数组的充分性:

很容易知道C8表示A1~A8的和,但是C6却是表示A5~A6的和,为什么会产生这样的区别的呢?或者说发明她的人为什么这样区别对待呢?答案是,这样会使操作更简单!看到这相信有些人就有些感觉了,为什么复杂度被log了呢?可以看到,C8可以看作A1~A8的左半边和+右半边和,而其中左半边和是确定的C4,右半边其实也是同样的规则把A5~A8一分为二……继续下去都是一分为二直到不能分树状数组巧妙地利用了二分,树状数组并不神秘,关键是巧妙!


三、树状数组的关键代码

1、

  1. int lowBit(int x)  
  2. {  
  3.     return x&(-x);  
  4. }  

这段代码可以简单的理解为是树状数组向前或向后衍生是用的。

向后主要是为了找到目前节点的父节点,比如要将C[4]+1,那么4+(4&(-4))=8,C[8]+1,8+(8&(-8))=16,

C[16]+1。

向前主要是为了求前缀和,比如要求A[1]+...+A[12]。那么,C[12]=A[9]+...+A[12];然后12-12&(-12)=8,

C[8]=A[1]+...+A[8]。

 

2、

  1. void modify(int pos,int num)  //pos为数组下标位置,num为要增加的值   
  2. {  
  3.     while(pos<=n)   //n为数组的长度   
  4.     {  
  5.         c[pos]+=num;  
  6.         pos+=lowBit(pos);  
  7.     }  
  8. }  

这段代码是用来更新树状数组的,包括区间更新、单点更新。

就是想刚才所说的,一点更新了,要不断将父节点也更新。

 

3、

[cpp]  view plain  copy
 print ?
  1. int getResult(int pos)  //求A[1]+...+A[pos]   
  2. {  
  3.     int sum=0;   
  4.     while(pos>0)  
  5.     {  
  6.         sum+=c[pos];  
  7.         pos-=lowBit(pos);  
  8.     }  
  9.       
  10.     return sum;   
  11. }                  

这段代码用来求解前缀和的。

就像刚才说的,求解A[1]+...+A[12],也就是C[12]+C[8]搞定。

 

四、树状数组的优点

1、原本的长度为n的数列求和时间复杂度为O(n),更改的时间复杂度为O(1)。

树状数组将复杂度优化为O(logn)。在n较大时,效率更高。

2、树状数组编码简单。

3、树状数组是一个可以很高效的进行区间统计的数据结构。在思想上类似于线段树空间复杂度略低,比线段树节省空间,编程复杂度比线段树低,但适用范围比线段树小,对可以进行的运算也有限制,比如每次要查询的是一个区间的最小值,似乎就没有很好的解决办法。

 

五、注意

1、树状数组的下标要从1开始。

2、pos-pos&(-pos)就到了下一个无联系的节点很容易理解。

为什么pos+pos&(-pos)就得到pos的父节点?这个根据 图 和 上面证明树状数组充分性的二分思想容易看出。

对于玄玄的东西,自己非要去搞复杂的数学证明,反而会扰了心智,如果不是数学家和算法工程师,大致知道如何得到的即可,对于大多数人,关键还是熟练运用算法。

 六、拓展

1、有的题目为改变的是一个区间,查询的反而是一个点,即区间更新,单点查询,表面上看,似乎和树状数组的使用方法恰好相反,实际上可以通过一个转化巧妙的解决。

首先对于每个数A定义集合up(A)表示{A, A+lowestbit(A), A+lowestbit(A)+lowestbit(A+lowestbit(A))...} 定义集合down(A)表示{A, A-lowestbit(A), A-lowestbit(A)-lowestbit(A-lowestbit(A)) ... , 0}。可以发现对于任何A<B,up(A)和down(B)的交集有且仅有一个数,则对于区间[A,B],如果更新up(A,data)和up(B+1,-data),查询down(C)就能达到要求。

2、无论单点更新、区间查询还是区间更新、单点查询,树状数组均能应用于多维的情况。

七、代码:

HDU1166

单点更新,区间求值

#include<iostream>   
  1. using namespace std;  
  2.   
  3. const int maxn=50001;  
  4.   
  5. int a[maxn];  
  6. int c[maxn];  
  7. int n;   
  8.   
  9. int lowBit(int t)  
  10. {  
  11.     return t&(-t);  
  12. }  
  13.   
  14. void modify(int t,int num)  
  15. {  
  16.     while(t<=n)  
  17.     {  
  18.         c[t]+=num;  
  19.         t+=lowBit(t);  
  20.     }  
  21. }  
  22.   
  23. int getResult(int t)  
  24. {  
  25.     int num=0;   
  26.     while(t>0)  
  27.     {  
  28.         num+=c[t];  
  29.         t-=lowBit(t);  
  30.     }  
  31.       
  32.     return num;   
  33. }  
  34.   
  35. void init()  
  36. {  
  37.     for(int i=1;i<=n;i++)  
  38.     {  
  39.         scanf("%d",&a[i]);  
  40.           
  41.         modify(i,a[i]);   
  42.     }  
  43. }  
  44.   
  45.   
  46. int main()  
  47. {  
  48.     int cas,Case=1;  
  49.       
  50.     scanf("%d",&cas);   
  51.     while(cas--)  
  52.     {   
  53.         memset(c,0,sizeof(c));   
  54.         printf("Case %d:\n",Case++);  
  55.            
  56.         scanf("%d",&n);  
  57.           
  58.         init();  
  59.           
  60.         char ch[15];  
  61.         int a,b;     
  62.         while(scanf("%s",&ch),strcmp(ch,"End"))  
  63.         {   
  64.             scanf("%d%d",&a,&b);  
  65.                
  66.             switch(ch[0])  
  67.             {  
  68.                 case 'Q':  
  69.                     printf("%d\n",getResult(b)-getResult(a-1));  
  70.                     break;   
  71.                 case 'A':   
  72.                     modify(a,b);  
  73.                     break;  
  74.                 case 'S':  
  75.                     modify(a,-b);  
  76.                     break;  
  77.             }  
  78.          }  
  79.      }  
  80.        
  81.      system("pause");  
  82.      return 0;  
  83. }   

 

HDU1556

区间更新,单点求值

#include<iostream>   
  1. #include<cstring>  
  2. using namespace std;  
  3.   
  4. const int maxn=100001;  
  5.   
  6. int c[maxn];  
  7. int n;  
  8.   
  9. int lowbit(int t)  
  10. {  
  11.     return t&(-t);  
  12. }  
  13.   
  14. void insert(int t,int d)  
  15. {  
  16.     while(t<=n)  
  17.     {  
  18.         c[t]+=d;  
  19.         t+=lowbit(t);  
  20.     }  
  21. }  
  22.   
  23. int getSum(int t)  
  24. {  
  25.     int sum=0;  
  26.     while(t>0)  
  27.     {  
  28.         sum+=c[t];  
  29.         t-=lowbit(t);  
  30.     }  
  31.       
  32.     return sum;  
  33. }  
  34.   
  35. int main()  
  36. {  
  37.     while(cin>>n,n)  
  38.     {  
  39.         int a,b;  
  40.         memset(c,0,sizeof(c));  
  41.           
  42.         for(int i=1;i<=n;i++)  
  43.         {  
  44.             scanf("%d%d",&a,&b);  
  45.               
  46.             insert(a,1);  
  47.             insert(b+1,-1);  
  48.         }  
  49.           
  50.        for(int j=1;j<n;j++)  
  51.        {  
  52.             printf("%d ",getSum(j));  
  53.        }  
  54.        printf("%d\n",getSum(n));  
  55.     }  
  56.       
  57.     system("pause");  
  58.     return 0;  
  59. }  

 

HDU2838

求逆序对

#include<iostream>   
  1. #include<cstring>  
  2. using namespace std;  
  3.   
  4. const int maxn=100001;  
  5.    
  6. struct node  
  7. {  
  8.     int cnt;  
  9.     __int64 sum;  
  10. }tree[maxn];           
  11.           
  12. int n;  
  13.   
  14. int lowBit(int x)  
  15. {  
  16.     return x&(-x);  
  17. }  
  18.   
  19. void modify(int x,int y,int t)  
  20. {  
  21.     while(x<=n)  
  22.     {  
  23.         tree[x].sum+=y;  
  24.         tree[x].cnt+=t;  //tree[].cnt来保存是否出现过a   
  25.         x+=lowBit(x);  
  26.     }  
  27. }  
  28.   
  29. __int64 query_cnt(int x)   //比x小的数的个数   
  30. {  
  31.     __int64 sum=0;  
  32.     while(x>0)  
  33.     {  
  34.         sum+=tree[x].cnt;  
  35.         x-=lowBit(x);  
  36.     }  
  37.       
  38.     return sum;  
  39. }  
  40.   
  41. __int64 query_sum(int x)  //比x小的所有数之和   
  42. {  
  43.     __int64 sum=0;  
  44.     while(x>0)  
  45.     {  
  46.         sum+=tree[x].sum;  
  47.         x-=lowBit(x);  
  48.     }  
  49.       
  50.     return sum;  
  51. }  
  52.   
  53. int main()  
  54. {  
  55.     while(~scanf("%d",&n))  
  56.     {  
  57.         int a;  
  58.         __int64 ans=0;   
  59.         memset(tree,0,sizeof(tree));  
  60.            
  61.         for(int i=1;i<=n;i++)  
  62.         {  
  63.             scanf("%d",&a);  
  64.               
  65.             modify(a,a,1);  //以a为下标更新数组   
  66.               
  67.             __int64 k1=i-query_cnt(a);   //k1为前i个数比a大的数的个数   
  68.             if(k1!=0)  
  69.             {  
  70.                 __int64 k2=query_sum(n)-query_sum(a); //目前所有数的和-目前所有比a小的数的和,为比a大的数的和     
  71.                 ans+=k1*a+k2;   //调换a所需的时间   
  72.             }  
  73.         }  
  74.           
  75.         printf("%I64d\n",ans);   
  76.     }  
  77.       
  78.     system("pause");  
  79.     return 0;  
  80. }   

 另一个题解:

分析:其实这个结果和逆序数有关,对某个位置i,如果前面比他大的有x个,那么a[i]至少要加x次 
如果后面有y个比a[i]小,那么a[i]至少要加y次,也就是说用两个树状数组来分别维护当前位置时前面有多少比他大,后面有多少个比他小

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

#define N 100001
#define ll long long

ll C[N],B[N];
ll num[N];
int T;

int Lowbit(int x){
    return x&(x^(x-1));
}

void add(ll C[],ll pos,ll num) {
    while(pos <= N) {//x最大是N
        C[pos] += num;
        pos += Lowbit(pos);
    }
}

ll Sum(ll C[],ll end) {
    ll sum = 0;
    while(end > 0) {
        sum += C[end];
        end -= Lowbit(end);
    }
    return sum;
}

int main() {
    int s, t, i, j, T;
    ll ans;
    while(~scanf("%d",&T)) {
        memset(C,0,sizeof(C));
        memset(B,0,sizeof(B));

        memset(num,0,sizeof(num));
        ans = 0;
        for(i = 1; i <= T; i ++) {
            scanf("%I64d",&num[i]);
            add(C,num[i],1);
            ans += num[i] *(i - Sum(C,num[i])) ;//计算当前点前面大于它的数的个数
        }
        for(i = T; i > 0; --i){//注意是从T至0的,
            ans += num[i] * Sum(B,num[i] - 1);//计算当前点后面小于它的个数
            add(B,num[i],1);//从后面算起,然后加起来,那么Sum求出来的就是它后面小于它的个数了
        }
        printf("%I64d\n",ans);

    }
    return 0;
}


八、二维树状数组

C[x][y]=sum(A[i][j])。其中,x-lowBit(x)+1<=i<=x,y-lowBit(y)+1<=j<=y。

例题:HDU1892

二维树状数组一般就是对矩阵的操作,更新、求值

代码:

#include<iostream>   
  1. #include<cstring>  
  2. using namespace std;  
  3.   
  4. const int maxn=1005;  
  5.   
  6. int c[maxn][maxn];  
  7.   
  8. int lowBit(int x)  
  9. {  
  10.     return x&(-x);  
  11. }  
  12.   
  13. void modify(int x,int y,int val)  
  14. {  
  15.     for(int i=x;i<maxn;i+=lowBit(i))  
  16.     {  
  17.         for(int j=y;j<maxn;j+=lowBit(j))  
  18.         {  
  19.             c[i][j]+=val;  
  20.         }  
  21.     }  
  22. }  
  23.   
  24. int getResult(int x,int y)  
  25. {  
  26.     int sum=0;  
  27.     for(int i=x;i>0;i-=lowBit(i))  
  28.     {  
  29.         for(int j=y;j>0;j-=lowBit(j))  
  30.         {  
  31.             sum+=c[i][j];  
  32.         }  
  33.     }  
  34.       
  35.     return sum;  
  36. }  
  37.   
  38. int getVal(int x,int y)  
  39. {  
  40.     return getResult(x,y)-getResult(x-1,y)-getResult(x,y-1)+getResult(x-1,y-1);  
  41. }  
  42.   
  43. void init()  
  44. {  
  45.     memset(c,0,sizeof(c));  
  46.       
  47.     for(int i=1;i<maxn;i++)  
  48.     {  
  49.         for(int j=1;j<maxn;j++)  
  50.         {  
  51.             modify(i,j,1);  
  52.         }  
  53.     }  
  54. }  
  55.       
  56. int main()  
  57. {  
  58.     int cas,cas1=1,query;  
  59.       
  60.     scanf("%d",&cas);  
  61.     while(cas--)  
  62.     {  
  63.         init();  
  64.           
  65.         scanf("%d",&query);  
  66.   
  67.         printf("Case %d:\n",cas1++);  
  68.         for(int i=1;i<=query;i++)  
  69.         {  
  70.             char ch;  
  71.             int x1,y1,x2,y2,n;  
  72.               
  73.             getchar();  
  74.             scanf("%c",&ch);  
  75.               
  76.             switch(ch)  
  77.             {  
  78.                 case 'S':  
  79.                     {  
  80.                     scanf("%d%d%d%d",&x1,&y1,&x2,&y2);  
  81.                     int x11=min(x1,x2);  
  82.                     int x22=max(x1,x2);  
  83.                     int y11=min(y1,y2);  
  84.                     int y22=max(y1,y2);  
  85.                     printf("%d\n",getResult(x22+1,y22+1)-getResult(x11,y22+1)-getResult(x22+1,y11)+getResult(x11,y11));  
  86.                     break;  
  87.                     }  
  88.                 case 'A':  
  89.                     {  
  90.                     scanf("%d%d%d",&x1,&y1,&n);  
  91.                     modify(x1+1,y1+1,n);  
  92.                     break;  
  93.                     }  
  94.                 case 'M':  
  95.                     {  
  96.                     scanf("%d%d%d%d%d",&x1,&y1,&x2,&y2,&n);  
  97.                     int v=getVal(x1+1,y1+1);  
  98.                     int Min=min(n,v);  
  99.                     modify(x1+1,y1+1,-Min);  
  100.                     modify(x2+1,y2+1,Min);  
  101.                     break;  
  102.                     }  
  103.                 case 'D':  
  104.                     {  
  105.                     scanf("%d%d%d",&x1,&y1,&n);  
  106.                     int v=getVal(x1+1,y1+1);  
  107.                     int Min=min(v,n);  
  108.                     modify(x1+1,y1+1,-Min);  
  109.                     break;  
  110.                     }  
  111.             }  
  112.         }  
  113.     }  
  114.       
  115.     system("pause");  
  116.     return 0;  
  117. }  


 

九、参考文章
http://mindlee.net/2011/07/10/binary-indexed-trees/ 

http://dongxicheng.org/structure/binary_indexed_tree/


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值