高级数据结构之树状数组

————————————————————————这些是转的,出处不明———————————————————————————————

   树状数组比较适合单个元素改变,反复求部分和,或者区间更新,单点求值。

   先看的是一维的树状数组。
  树状数组是一个很天才的想法,考虑这样的一种情景,对于一组数据,你经常要求他们某个区间的和,而却这组数据里的元素会经常的改变,最朴素的想法就是暴力,O(1)的修改,O(n)的查询,或O(n)的修改O(1)的查询(就是记录)。第二种想法就是线段树,查询和修改的复杂度都是O(logn),线段树的编程复杂度比较高,常数因子也较大。有一种时间复杂度也是O(logn)的而且编程复杂度很简单的方法,就是用树状数组。树状数组的灵感是来源于二进制、线段树和O(1)查询O(n)修改算法(其实是我自己的灵感啦哈哈),二进制有01组成,每一个数字都有自己对应的一个二进制,既然线段数是把数据按二分的思想,把区间分成两个一样大小的区间,把大问题分解成两个小的子问题,那么在一组规模大小是10010110的数据,同样的我们也可以把区间分成一个个子区间,把大问题分解成一个个小问题,那么要怎么分解呢?看这二进制就明白了,我们要把区间分解成一个个大小不一的子区间,使它们加起来刚好就是原来的区间,很明显这个二进制可以分成:

如果我们要求1到10010100的和,即可用四个子区间组成原问题
[1,10]
[11,100]
[1001,10000]
[10001,10010100]
把它们加起来就是了,这就是说我们每次都把10010110最右边的“1”拿出来,作为子区间的右边界,左边界就是前一个子区间的右边界加1,第一个子区间的左边界是1.用位运算,把最右边的1分解,i&(i^(i-1))即i&(-i),这是自低向上的把区间分解出来,接下来就是定义tree[i]来递推了,按照上面的分法,很难定义,所以我们自顶向下的分区间试试看能不能容易的定义,分的四个区间:
[10010101,10010110]
[10010001,10010100]   
[10000001,10010000]
[00000001,10000000]
这样定义起来就很方便了,我們可以看到右區間就是每往下都去掉一個最右邊的1,tree[i]就是区间[i-(i&-i)+1,i]的和,由这里的定义知道,树状数组要从1开始计数,而不是c/c++一向的0开始。求和函数就这样写:

1 int get_sum(int i)

2 {

3     int sum = 0;

4     while(i) {

5             sum += tree[i];

6             i -= i & -i;

7     }

8     return sum;

9 } 
View Code

 

这样的话,求区间[i,j]的值就是get_sum(j)-get_sum(i-1)了。如果经常求一个单个元素的指,这样写就重复计算了。我们可以在tree[i]所覆盖的范围中,减去除a[i]以外的(一般树状数组是不保存原始数据a[i]的,一般而已),因为每次都是减去最右边的1然后加1,就是减的范围不能小于等于i-(i&-i),很明显,i-1>=i-(i&-i),也就是说只要这个数字i'在范围内(不包括等于),那么它所覆盖的范围不会超过i-(i&-i),因为它怎么减去最右边的1,至少等于i-(i&-i),而覆盖范围的下界是i'-(i'&-i')+1,所以我们可以这样写:

 1 int get_single(int i)

 2 {

 3     int s = tree[i],z=i-(i&-i);

 4     --i;

 5     while(i>z) {

 6                 s -= tree[i];

 7             i -= i&-i;

 8     }

 9     return s;

10 }
View Code

 

查询已经搞定了,现在就看看修改的时候要怎么做了。先看看树状数组的图是怎样的。

 高级数据结构之树状数组


  很明显,当我修改了a[i]的值,那么最小的,收到影响的首当其冲是tree[i],接着就是一级级往上影响它的父亲节点。所以这里的重点就是在于怎么找一个节点的父亲节点。我们知道,一个节点的父亲节点序号肯定比它大,那么我们就是要找一个范围能覆盖i的最小的j,那个j就是i的父节点,那么就很明显i的父节点不会是由i最右边1的右边的0变成1变成的,因为那样的j把最右1去掉再加1后,覆盖范围刚好覆盖不到i,所以就只能由最右边1的左边的0变成1,而现在要找的是最小的,因此我们只要把加上一个最右1就可以了,代码这样写:

1 void modify(int i,int c)

2 {

3     while(i<=MIX) { //MIX代表树状数组最大的编号

4     tree[i] += c;

5     i += i&-i;

6      }

7 }
View Code

 

查询和修改的时间复杂度都是O(logn)。

   树状数组的主要操作函数就是修改&&求和,那么就是说还可以用在统计计数方面的情景。通常对于这些统计计数的情景,遍历的顺序挺重要的,有时候前到后遍历简单,有时候后到前遍历简单。
   一维的树状数组就是这样,下面就先来看两题水题,来看看怎么用树状数组,poj2352。
题目的大意就是说给你一堆星星的坐标(x,y),然后要你输出每一层的星星的数量,层的意思是有多少颗星的x和y不大于这颗星星。由于他输入的时候已经是按y从小到大的输入,所以层数的计算,我们只需要判断当前输入的这颗星星的x坐标,大过他前面输入的多少颗星星就可以了。所以我们令tree[i]是x坐标是i的星星个数,要求层数的时候,就get_sum就可以了。具体的代码如下:

 1 #include<stdio.h>

 2 #include<memory.h>

 3 #define MIX 32001

 4 int tree[MIX],n,result[15000];

 5 int lowbit(int x) {return x&(-x);}

 6 int get_sum(int k)

 7 {

 8     int s=0;

 9     while(k>0) {

10         s+=tree[k];

11         k-=lowbit(k);

12     }

13     return s;

14 }

15 void modify(int pos,int a)

16 {

17     while(pos<=MIX) {

18         tree[pos]+=a;

19         pos+=lowbit(pos);

20     }

21 }

22 int main(void)

23 {

24     int i,x,y;

25     memset(tree,0,sizeof(tree));

26     memset(result,0,sizeof(result));

27     scanf("%d",&n);

28     for(i=0; i<n; ++i){

29         scanf("%d %d",&x,&y);

30         ++x;

31         ++result[get_sum(x)];

32         modify(x,1);

33     }

34     for(i=0; i<n; ++i) printf("%d\n",result[i]);

35     return 0;

36 }
View Code

 

  下面再来看一题目poj2299,这题目的转化后的意思就是给你一组数字,然后要你求这组数中的逆序对有多少。有两种方法做这题,一种是归并排序变形,一种就是树状数组,归并排序的方法是算法导论上的习题,在CLRS总结上面有这里就不说了,只说树状数组的方法。如何数据范围不大的话,我们就可以直接定义tree[i]代表数字i的个数,然后从后往前的遍历,这样就可以知道每个数字排在它后面却比它小的数字有多少个了,累加就可以了。也可以从前往后遍历,不过这时候get_sum(i)的值代表的是a[i]前面小于等于a[i]的有多少个,i-get_sum(i)就是大于在a[i]前面并且大于他的数了。然而这里的数据范围很打,a[i]的值能取到999999999,我们不可能开一个这么大的数组,所以在这里我们要用离散化先处理数组,离散化的意思,就是把原来的值建立一个新的一一映射,使范围减少,建立一个紧凑的范围,减少空间,在范围大而数据数量相对比较少的情况下很使用,在这题中,例如99999999 1 123 1583,我们建立的一一映射就是4 1 2 3,然后按照这个新的映射关系和之前的做法一样。代码如下:

 1 #include<stdio.h>

 2 #include<memory.h>

 3 #define MIX 500001

 4 struct node {

 5     int v,idx;

 6 } a[MIX];

 7 int  tree[MIX],f[MIX];

 8 int lowbit(int x) {return x&(-x);}

 9 void modify(int x,int b)

10 {

11     while(x<=MIX) {

12         tree[x] += b;

13         x += lowbit(x);

14     }

15 }

16 int get_sum(int x)

17 {

18     int sum = 0;

19     while(x) {

20         sum += tree[x];

21         x -= lowbit(x);

22     }

23     return sum;

24 }

25 

26 void swap(struct node* a,struct node *b)

27 {

28     struct node temp = *a; *a = *b; *b = temp;

29 }

30 

31 int med(struct node *a,int low,int hight)

32 {

33     int center = (low+hight)>>1;

34     if(a[center].v > a[hight].v)

35         swap(&a[center],&a[hight]);

36     if(a[low].v > a[hight].v)

37         swap(&a[low],&a[hight]);

38     if(a[low].v < a[center].v )

39         swap(&a[low],&a[center]);

40     return a[low].v;

41 }

42 

43 void myqsort(struct node *a,int low,int hight)

44 {

45     if(low<hight) {

46         int i = low, j = hight,x=med(a,low,hight);

47         struct node tmp;

48         for(;;) {

49             while (a[++i].v < x) ;

50             while (a[--j].v > x) ;

51             if(i<j) {

52                 tmp = a[i]; a[i] = a[j]; a[j] = tmp;

53             } else {

54                 tmp = a[low]; a[low] = a[j]; a[j] = tmp;

55                 break;

56             }

57         }

58         myqsort(a,low,j-1);

59         myqsort(a,j+1,hight);

60     }

61 }

62 int main(void)

63 {

64     int n,i;

65     long long sum;

66     a[0].v = -999999;

67     while(scanf("%d",&n),n) {

68         memset(tree,0,sizeof(tree));

69         for(i=1; i<=n; ++i) {

70             scanf("%d",&a[i].v);

71             a[i].idx = i;

72         }

73         myqsort(a,1,n);

74         sum = 0;

75         for(i=1; i<=n; ++i) {

76             f[a[i].idx] = i;

77         for(i=n; i; --i) {

78             sum += get_sum(f[i]);

79             modify(f[i],1);

80         }

81         printf("%lld\n",sum);

82     }

83 }
View Code

 

  上面两题都是单点更新,区间求和的,下面来看看树状数组是怎么区间更新,单点求值。Hdu1556,题意就是说总共有N个数,每次给个一个区间[a,b]给你,区间内的数全部+1,N次之后,要求输出每一个位置上的值。朴素的方法是每次遍历区间,+1,这样的复杂度是O(testcases*N*N),想不超时都难。这题的概述第一时间想到的是线段树,不过也可以用树状数组,甚至不用树状数组,直接用数组。这里要思考的是有没有一种修改方法不用遍历区间呢,不遍历就能达成遍历的效果,就相当于我修改一个值,后面的值也会受到影响,貌似用求和的思想可以达成影响后面的值,那么就是说,假设num[a]到num[b]要+1,只需要我们想一下tree[i]的定义,我们定义tree[i]代表对[i,N]的贡献,那么每当[a,b]要+1的时候,我们就可以tree[a]+=1,这是从[b+1,N]都多加了1,所以要tree[b+1]-=1。所以当要输出位置i加了多少次,就是get_sum(i),这里是抽象的来看,如果微观的看的话,要注意tree是怎么加的,不是累加,所以不会变多。在这题里是全部都输入之后,再从头输出i的值,所以就直接开一个数组就行了。可是如果是输入中间夹杂着多次查询的话,就要用树状数组了,很数组差不多[a,b]要加1,就modify(a,1),modify(b+1,-1)。下面是树状数组的代码:

 1 #include<stdio.h>

 2 #include<memory.h>

 3 #define MIX 100001

 4 int tree[MIX];

 5 void modify(int i,int c)

 6 {

 7     while(i<=MIX) {

 8         tree[i] += c;

 9         i += i & -i;

10     }

11 }

12 int get_sum(int i)

13 {

14     int sum = 0;

15     while(i) {

16         sum += tree[i];

17         i -= i& -i;

18     }

19     return sum;

20 }

21 int main(void)

22 {

23     int n,a,b,i;

24     while(scanf("%d",&n),n) {

25         memset(tree,0,sizeof(tree));

26         for(i=0; i<n; ++i) {

27             scanf("%d%d",&a,&b);

28             modify(a,1);

29                     modify(b+1,-1);

30         }

31         for(i=1; i<n; ++i) 

32             printf("%d ",get_sum(i));

33         printf("%d\n",get_sum(n));

34     }

35     return 0;

36 }
View Code

 

  一维的树状数组大概就是这样了,现在说二维的树状数组。二维树状数组对应的是矩阵,是一维的扩展,一般用来快速求子矩阵的和,在二维树状数组中,tree[x][y]代表的是左上角是(x-lowbit(x)+1,y-lowbit(y)+1),右下角是(x,y)的矩阵的和。很明显求左上角是(1,1),右下角是(x,y)的求和就是二重循环枚举x,y,一个个子矩阵的叫上去。代码:

 1 int get_sum(int x,int y)

 2 {

 3     int sum=0,y1;

 4     while(x) {

 5         y1 = y;

 6         while(y1) {

 7             sum += tree[x][y1];

 8             y1 -= y1 & -y1;

 9         }

10         x -= x & -x;

11     }

12 }
View Code

 

modify函数也是差不多的

 1 void modify(int x,int y,int val)

 2 {

 3     while(x<=MAX_X) {

 4         int y1 = y;

 5         while(y1<=MAX_Y) {

 6             tree[x][y1] += val;

 7             y1 += y1 & -y1;

 8         }

 9         x += x & -x; 

10     }

11 }
View Code

 

查询和修改的时间复杂度是O(logMAX_X * logMAX_Y)。下面就看看怎么用了,poj2215,题目大意是一个N*N矩阵,初始0,有两个操作,一个是C x1 y1 x2 y2,就是把左上角是(x1,y1)右下角是(x2,y2)的子矩阵的每一位取反(0变1,1变0).这样和上面那题其实是差不多的,我们不用真的记录矩阵的真实值,只记录变化了多少次就可以了。因为一开始是0,所以就是说变化的次数是偶数就是0,是奇数就是1.tree[x][y]的数值代表(x,y)到(n,n)导致了多少变化,和上题一样modify时会把不应该变的也变了,所以还要变回来。代码如下:

 1 #include<stdio.h>

 2 #include<string.h>

 3 #define N 1000

 4 int tree[N+1][N+1],n;

 5 void modify(int x,int y,int val)

 6 {

 7     while(x<=n) {

 8         int y1 = y;

 9         while(y1<=n) {

10             tree[x][y1] += val; 

11             y1 += y1&-y1;

12         }

13         x += x&-x;

14     }

15 }

16 int get_sum(int x,int y)

17 {

18     int sum = 0,y1;

19     while(x) {

20         y1 = y;

21         while(y1) {

22             sum += tree[x][y1];

23             y1 -= y1&-y1;

24         }

25         x -= x&-x;

26     }

27     return sum;

28 }

29 int main(void) 

30 {

31     int T,X,x1,y1,x2,y2;

32     char c;

33     scanf("%d",&X);

34      getchar();

35     while(X--) {

36         memset(tree,0,sizeof(tree));

37         scanf("%d%d",&n,&T);

38          getchar();

39         while(T--) {

40             scanf("%c %d %d",&c,&x1,&y1);

41              getchar();

42             if(c=='C') {

43                 scanf("%d %d",&x2,&y2);

44                 getchar();

45                 modify(x1,y1,1);

46                 modify(x1,y2+1,1);

47                 modify(x2+1,y1,1);

48                 modify(x2+1,y2+1,1);

49             } else 

50                 printf("%d\n",get_sum(x1,y1)&1);    

51         }

52         putchar('\n');

53     }

54 }
View Code

 

   HDU4267,12年长春网络赛的题,是挺好的一题,题目大意是先给你一组数,然后有两个操作,一个是1 a b k c,意思是将[a,b]内符合(i-a)%k=0的位置都加上c,另一个操作是2 a,意思是查询位置a的值是多少。这题是区间内离散位置的更新,然后是单点求值。而无论是线段树还是树状数组的区间更新都是连续的更新,这里是离散,所以肯定一棵树是解决不了的。这里我们用线段树来解决这个问题,先看看有多少情况,1<=k<=10,k有10种情况,mod k有10种情况,那么总共就有100种情况了,那么我们就维护100个树状数组,每次只更新一个,查询的时候,就把相应的加起来就是了。所以我们一样用区间更新,单点取值的方法来做这题目,也就是get_sum(i)代表i位置上的值。那么就是tree[k][x%k][x],这里其实我们可以令q=(k-1)*10+x%k,也能分离出各种不同情况,只需要开一个的二维的就能代替原来的3维了。代码如下:

 1 #include<stdio.h> 

 2 #include<memory.h> 

 3 #define MIX 50000 

 4 int n,num[MIX+1]; 

 5 int tree[100][MIX+1]; 

 6 void modify(int k,int i,int val) 

 7 { 

 8     while(i<=n) { 

 9         tree[k][i] += val; 

10         i += i&-i; 

11     } 

12 } 

13 

14 int get_sum(int k,int i) 

15 { 

16     int sum = 0; 

17     while(i) { 

18         sum+=tree[k][i]; 

19         i -= i&-i; 

20     } 

21     return sum; 

22 } 

23 

24 int main(void) 

25 { 

26     int i,Q,a,b,k,c,p; 

27     while(scanf("%d",&n)!=EOF) { 

28         memset(tree,0,sizeof(tree)); 

29         for(i=1; i<=n; ++i) scanf("%d",num+i); 

30         scanf("%d",&Q); 

31         while(Q--) { 

32             scanf("%d",&p); 

33             if(p==2) { 

34                 scanf("%d",&p); 

35                 a = num[p]; 

36                 for(i=1; i<=10; ++i) 

37                     a += get_sum((i-1)*10+p%i,p); 

38                 printf("%d\n",a); 

39             } else { 

40                 scanf("%d%d%d%d",&a,&b,&k,&c); 

41                 b -= (b-a)%k; 

42                 modify((k-1)*10+a%k,a,c); 

43                 modify((k-1)*10+b%k,b+1,-c); 

44             } 

45         } 

46     } 

47 } 
View Code

 

这个方法的空间占用比较大,因为每棵树都有大量的空间是浪费的,不会怎么用到的,按照k的范围,我们只开k课树,在第k课树更新的时候[a,b]的时候,会把区间里面不应该变化的点也变化了,如果我们把这些离散的点,映射成一个个连续的点就可以解决问题了,在第k课树,把1,1+k,1+2k...; 2,2+k,2+2k...等等都连续的放在一起,那么更新的时候就不会把不该更新的也更新了。那么映射就是1~1,1+k~2,1+2k~3...建立映射的代码如下:

 1 void init() 

 2 { 

 3     int k,i,j,s; 

 4     for(k=1; k<=10; ++k) { 

 5         s = 1; 

 6         for(i=1;i<=k; ++i) { 

 7             for(j=i; j<=MIX; j+=k) 

 8             f[k][j] = s++; 

 9         } 

10     } 

11 }
View Code

 

注意,当应用了这个映射的方法之后,modify要上溯到MIX才行,因为原来的位置打乱了。

现在来总结一下树状数组
作用在统计求和,根据这个和代表的东西不同,灵活应用来解决问题。求和和更新的时间复杂度都是O(logn)。
通常有两种用法:1、单点更新,区间求和  
        此时tree[i]代表[i-i&-i,i]的和,get_sum(i)代表[1,i]的和
        2、区间更新,单点求值  多个modify,把多加的减回去。  
        此时tree[i]表示[i-i&-i,i]对[i,MIX]的贡献,get_sum(i)代表i位置上的值
对于二维的树状数组,和一维的差不多,只不过是用于矩阵的求和。

———————————————————————以下是补充———————————————————————————————

树状数组也可以做到区间修改和区间查询。

给区间[l, r]同时加上x,令:

s(i) = 加上x之前的sum{a[1..i]}

s`(i) = 加上x之后的sum{a[1..i]}

那么,有:

where i < l → s`(i) = s(i)

where l ≤ i ≤ r →s`(i) = s(i) + x * (i - l + 1) = s(i) + x * i - x * (l - 1)

where r < i → s`(i) = s(i) + x * (r - l + 1)

令sum(bit, i)为树状数组bit的前 i 项和。构建两个数组bit0和bit1,并设:

sum{a[1..i]} = sum(bit1, i) * i + sum(bit0, i)

那么,要给[l, r]同时加上x,那么有:

在bit0的l位置加上-x(l-1)

在bit1的l位置加上x

在bit0的r+1位置加上xr

在bit1的r+1位置加上-x

便能在O(logn)实现对树状数组的更新和查询操作。

然后我们来看一道题,POJ3468 A Simple Problem with Integers。

题目大意:给n个数,q个询问,每次给一个区间加上同一个值,或者询问一个区间和。

然后就是这个树状数组的裸题咯,直接上代码吧。

虽然修改的时候,c*r不会爆int,但是读入a数组的时候,a*i会爆int请注意……

代码(1985MS)(为何POJ的G++会比C++慢一倍……):

 1 #include <iostream>

 2 #include <cstdio>

 3 #include <cstring>

 4 #include <algorithm>

 5 using namespace std;

 6 typedef long long LL;

 7 

 8 const int MAXN = 100010;

 9 

10 LL bit0[MAXN], bit1[MAXN];

11 int n, q;

12 

13 inline int lowbit(int x) {

14     return x & -x;

15 }

16 

17 void modify(LL *bit, int k, LL val) {

18     while(k <= n) {

19         bit[k] += val;

20         k += lowbit(k);

21     }

22 }

23 

24 LL get_sum(LL *bit, int k) {

25     LL ret = 0;

26     while(k) {

27         ret += bit[k];

28         k -= lowbit(k);

29     }

30     return ret;

31 }

32 

33 void modify(int l, int r, LL val) {

34     modify(bit0, l, - val * (l - 1));

35     modify(bit1, l, val);

36     modify(bit0, r + 1, val * r);

37     modify(bit1, r + 1, -val);

38 }

39 

40 LL get_sum(int l, int r) {

41     LL sum1 = get_sum(bit1, l - 1) * (l - 1) + get_sum(bit0, l - 1);

42     LL sum2 = get_sum(bit1, r) * r + get_sum(bit0, r);

43     return sum2 - sum1;

44 }

45 

46 int main() {

47     int l, r, a;

48     scanf("%d%d", &n, &q);

49     for(int i = 1; i <= n; ++i)

50         scanf("%d", &a), modify(i, i, a);

51     while(q--) {

52         char c;

53         scanf(" %c", &c);

54         if(c == 'C') {

55             scanf("%d%d%d", &l, &r, &a);

56             modify(l, r, a);

57         } else {

58             scanf("%d%d", &l, &r);

59             printf("%I64d\n", get_sum(l, r));

60         }

61     }

62 }
View Code

 

你可能感兴趣的:(数据结构)