[关闭]
@RabbitHu 2017-09-24T14:27:14.000000Z 字数 4397 阅读 1948

“高级”数据结构——树状数组!

笔记


※本文一切代码未经编译,不保证正确性,如发现问题,欢迎指正!

1. 单点修改 + 区间查询

最简单的树状数组就是这样的:

  1. void add(int p, int x){ //给位置p增加x
  2. while(p <= n) sum[p] += x, p += p & -p;
  3. }
  4. int ask(int p){ //求位置p的前缀和
  5. int res = 0;
  6. while(p) res += sum[p], p -= p & -p;
  7. return res;
  8. }
  9. int range_ask(int l, int r){ //区间求和
  10. return ask(r) - ask(l - 1);
  11. }

2. 区间修改 + 单点查询

通过“差分”(就是记录数组中每个元素与前一个元素的差),可以把这个问题转化为问题1。

查询

设原数组为, 设数组,则 ,可以通过求的前缀和查询。

修改

当给区间加上x的时候, 与前一个元素 的差增加了 的差减少了。根据数组的定义,只需给 加上 , 给 减去 即可。

  1. void add(int p, int x){ //这个函数用来在树状数组中直接修改
  2. while(p <= n) sum[p] += x, p += p & -p;
  3. }
  4. void range_add(int l, int r, int x){ //给区间[l, r]加上x
  5. add(l, x), add(r + 1, -x);
  6. }
  7. int ask(int p){ //单点查询
  8. int res = 0;
  9. while(p) res += sum[p], p -= p & -p;
  10. return res;
  11. }

3. 区间修改 + 区间查询

这是最常用的部分,也是用线段树写着最麻烦的部分——但是现在我们有了树状数组!

怎么求呢?我们基于问题2的“差分”思路,考虑一下如何在问题2构建的树状数组中求前缀和:

位置p的前缀和 =

在等式最右侧的式子中, 被用了次,被用了次……那么我们可以写出:

位置p的前缀和 =

那么我们可以维护两个数组的前缀和:
一个数组是
另一个数组是

查询

位置p的前缀和即: (p + 1) * sum1数组中p的前缀和 - sum2数组中p的前缀和。

区间[l, r]的和即:位置r的前缀和 - 位置l的前缀和。

修改

对于sum1数组的修改同问题2中对d数组的修改。

对于sum2数组的修改也类似,我们给 sum2[l] 加上 l * x,给 sum2[r + 1] 减去 (r + 1) * x。

  1. void add(ll p, ll x){
  2. for(int i = p; i <= n; i += i & -i)
  3. sum1[i] += x, sum2[i] += x * p;
  4. }
  5. void range_add(ll l, ll r, ll x){
  6. add(l, x), add(r + 1, -x);
  7. }
  8. ll ask(ll p){
  9. ll res = 0;
  10. for(int i = p; i; i -= i & -i)
  11. res += (p + 1) * sum1[i] - sum2[i];
  12. return res;
  13. }
  14. ll range_ask(ll l, ll r){
  15. return ask(r) - ask(l - 1);
  16. }

用这个做区间修改区间求和的题,无论是时间上还是空间上都比带lazy标记的线段树要优。

4. 二维树状数组

我们已经学会了对于序列的常用操作,那么我们不由得想到(谁会想到啊喂)……能不能把类似的操作应用到矩阵上呢?这时候我们就要写二维树状数组了!

在一维树状数组中,tree[x](树状数组中的那个“数组”)记录的是右端点为x、长度为lowbit(x)的区间的区间和。
那么在二维树状数组中,可以类似地定义tree[x][y]记录的是右下角为(x, y),高为lowbit(x), 宽为 lowbit(y)的区间的区间和。

单点修改 + 区间查询

  1. void add(int x, int y, int z){ //将点(x, y)加上z
  2. int memo_y = y;
  3. while(x <= n){
  4. y = memo_y;
  5. while(y <= n)
  6. tree[x][y] += z, y += y & -y;
  7. x += x & -x;
  8. }
  9. }
  10. void ask(int x, int y){//求左上角为(1,1)右下角为(x,y) 的矩阵和
  11. int res = 0, memo_y = y;
  12. while(x){
  13. y = memo_y;
  14. while(y)
  15. res += tree[x][y], y -= y & -y;
  16. x -= x & -x;
  17. }
  18. }

区间修改 + 单点查询

我们对于一维数组进行差分,是为了使差分数组前缀和等于原数组对应位置的元素。

那么如何对二维数组进行差分呢?可以针对二维前缀和的求法来设计方案。

二维前缀和:

那么我们可以令差分数组 表示 的差。

例如下面这个矩阵

 1  4  8
 6  7  2
 3  9  5

对应的差分数组就是

 1  3  4
 5 -2 -9
-3  5  1

当我们想要将一个矩阵加上x时,怎么做呢?
下面是给最中间的3*3矩阵加上x时,差分数组的变化:

0  0  0  0  0
0 +x  0  0 -x
0  0  0  0  0
0  0  0  0  0
0 -x  0  0 +x

这样给修改差分,造成的效果就是:

0  0  0  0  0
0  x  x  x  0
0  x  x  x  0
0  x  x  x  0
0  0  0  0  0

那么我们开始写代码吧!

  1. void add(int x, int y, int z){
  2. int memo_y = y;
  3. while(x <= n){
  4. y = memo_y;
  5. while(y <= n)
  6. tree[x][y] += z, y += y & -y;
  7. x += x & -x;
  8. }
  9. }
  10. void range_add(int xa, int ya, int xb, int yb, int z){
  11. add(xa, ya, z);
  12. add(xa, yb + 1, -z);
  13. add(xb + 1, ya, -z);
  14. add(xb + 1, yb + 1, z);
  15. }
  16. void ask(int x, int y){
  17. int res = 0, memo_y = y;
  18. while(x){
  19. y = memo_y;
  20. while(y)
  21. res += tree[x][y], y -= y & -y;
  22. x -= x & -x;
  23. }
  24. }

区间修改 + 区间查询

类比之前一维数组的区间修改区间查询,下面这个式子表示的是点(x, y)的二维前缀和:

(d[h][k]为点(h, k)对应的“二维差分”(同上题))

这个式子炒鸡复杂( 复杂度!),但利用树状数组,我们可以把它优化到

首先,类比一维数组,统计一下每个出现过多少次。出现了次,出现了次…… 出现了 次。

那么这个式子就可以写成:

把这个式子展开,就得到:

那么我们要开四个树状数组,分别维护:

这样就完成了!

  1. #include <cstdio>
  2. #include <cmath>
  3. #include <cstring>
  4. #include <algorithm>
  5. #include <iostream>
  6. using namespace std;
  7. typedef long long ll;
  8. ll read(){
  9. char c; bool op = 0;
  10. while((c = getchar()) < '0' || c > '9')
  11. if(c == '-') op = 1;
  12. ll res = c - '0';
  13. while((c = getchar()) >= '0' && c <= '9')
  14. res = res * 10 + c - '0';
  15. return op ? -res : res;
  16. }
  17. const int N = 205;
  18. ll n, m, Q;
  19. ll t1[N][N], t2[N][N], t3[N][N], t4[N][N];
  20. void add(ll x, ll y, ll z){
  21. for(int X = x; X <= n; X += X & -X)
  22. for(int Y = y; Y <= m; Y += Y & -Y){
  23. t1[X][Y] += z;
  24. t2[X][Y] += z * x;
  25. t3[X][Y] += z * y;
  26. t4[X][Y] += z * x * y;
  27. }
  28. }
  29. void range_add(ll xa, ll ya, ll xb, ll yb, ll z){ //(xa, ya) 到 (xb, yb) 的矩形
  30. add(xa, ya, z);
  31. add(xa, yb + 1, -z);
  32. add(xb + 1, ya, -z);
  33. add(xb + 1, yb + 1, z);
  34. }
  35. ll ask(ll x, ll y){
  36. ll res = 0;
  37. for(int i = x; i; i -= i & -i)
  38. for(int j = y; j; j -= j & -j)
  39. res += (x + 1) * (y + 1) * t1[i][j]
  40. - (y + 1) * t2[i][j]
  41. - (x + 1) * t3[i][j]
  42. + t4[i][j];
  43. return res;
  44. }
  45. ll range_ask(ll xa, ll ya, ll xb, ll yb){
  46. return ask(xb, yb) - ask(xb, ya - 1) - ask(xa - 1, yb) + ask(xa - 1, ya - 1);
  47. }
  48. int main(){
  49. n = read(), m = read(), Q = read();
  50. for(int i = 1; i <= n; i++){
  51. for(int j = 1; j <= m; j++){
  52. ll z = read();
  53. range_add(i, j, i, j, z);
  54. }
  55. }
  56. while(Q--){
  57. ll ya = read(), xa = read(), yb = read(), xb = read(), z = read(), a = read();
  58. if(range_ask(xa, ya, xb, yb) < z * (xb - xa + 1) * (yb - ya + 1))
  59. range_add(xa, ya, xb, yb, a);
  60. }
  61. for(int i = 1; i <= n; i++){
  62. for(int j = 1; j <= m; j++)
  63. printf("%lld ", range_ask(i, j, i, j));
  64. putchar('\n');
  65. }
  66. return 0;
  67. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注