二维数组数组详解

 

转载自: 胡小兔博客,https://www.cnblogs.com/RabbitHu/p/BIT.html

%%大连市理科状元。 

 

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

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

1. 单点修改 + 区间查询

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

void add(int p, int x){ //给位置p增加x while(p <= n) sum[p] += x, p += p & -p; } int ask(int p){ //求位置p的前缀和 int res = 0; while(p) res += sum[p], p -= p & -p; return res; } int range_ask(int l, int r){ //区间求和 return ask(r) - ask(l - 1); }

2. 区间修改 + 单点查询

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

查询

设原数组为a[i]a[i], 设数组d[i]=a[i]a[i1](a[0]=0)d[i]=a[i]−a[i−1](a[0]=0),则 a[i]=ij=1d[j]a[i]=∑j=1id[j],可以通过求d[i]d[i]的前缀和查询。

修改

当给区间[l,r][l,r]加上x的时候,a[l]a[l] 与前一个元素 a[l1]a[l−1] 的差增加了xx,a[r+1]a[r+1] 与 a[r]a[r] 的差减少了xx。根据d[i]d[i]数组的定义,只需给d[l]d[l] 加上 xx, 给d[r+1]d[r+1] 减去 xx 即可。

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

3. 区间修改 + 区间查询

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

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

位置p的前缀和 =

i=1pa[i]=i=1pj=1id[j]∑i=1pa[i]=∑i=1p∑j=1id[j]

在等式最右侧的式子pi=1ij=1d[j]∑i=1p∑j=1id[j]中,d[1]d[1] 被用了pp次,d[2]d[2]被用了p1p−1次……那么我们可以写出:

位置p的前缀和 =

i=1pj=1id[j]=i=1pd[i](pi+1)=(p+1)i=1pd[i]i=1pd[i]i∑i=1p∑j=1id[j]=∑i=1pd[i]∗(p−i+1)=(p+1)∗∑i=1pd[i]−∑i=1pd[i]∗i

那么我们可以维护两个数组的前缀和:
一个数组是 sum1[i]=d[i]sum1[i]=d[i],
另一个数组是 sum2[i]=d[i]isum2[i]=d[i]∗i。

查询

位置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。

void add(ll p, ll x){
    for(int i = p; i <= n; i += i & -i) sum1[i] += x, sum2[i] += x * p; } void range_add(ll l, ll r, ll x){ add(l, x), add(r + 1, -x); } ll ask(ll p){ ll res = 0; for(int i = p; i; i -= i & -i) res += (p + 1) * sum1[i] - sum2[i]; return res; } ll range_ask(ll l, ll r){ return ask(r) - ask(l - 1); }

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

4. 二维树状数组

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

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

单点修改 + 区间查询

void add(int x, int y, int z){ //将点(x, y)加上z int memo_y = y; while(x <= n){ y = memo_y; while(y <= n) tree[x][y] += z, y += y & -y; x += x & -x; } } void ask(int x, int y){//求左上角为(1,1)右下角为(x,y) 的矩阵和 int res = 0, memo_y = y; while(x){ y = memo_y; while(y) res += tree[x][y], y -= y & -y; x -= x & -x; } }

区间修改 + 单点查询

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

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

二维前缀和:

sum[i][j]=sum[i1][j]+sum[i][j1]sum[i1][j1]+a[i][j]sum[i][j]=sum[i−1][j]+sum[i][j−1]−sum[i−1][j−1]+a[i][j]

那么我们可以令差分数组d[i][j]d[i][j] 表示 a[i][j]a[i][j] 与 a[i1][j]+a[i][j1]a[i1][j1]a[i−1][j]+a[i][j−1]−a[i−1][j−1] 的差。

例如下面这个矩阵

 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

那么我们开始写代码吧!

void add(int x, int y, int z){ int memo_y = y; while(x <= n){ y = memo_y; while(y <= n) tree[x][y] += z, y += y & -y; x += x & -x; } } void range_add(int xa, int ya, int xb, int yb, int z){ add(xa, ya, z); add(xa, yb + 1, -z); add(xb + 1, ya, -z); add(xb + 1, yb + 1, z); } void ask(int x, int y){ int res = 0, memo_y = y; while(x){ y = memo_y; while(y) res += tree[x][y], y -= y & -y; x -= x & -x; } }

区间修改 + 区间查询

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

i=1xj=1yk=1ih=1jd[h][k]∑i=1x∑j=1y∑k=1i∑h=1jd[h][k]
(d[h][k]为点(h, k)对应的“二维差分”(同上题))

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

首先,类比一维数组,统计一下每个d[h][k]d[h][k]出现过多少次。d[1][1]d[1][1]出现了xyx∗y次,d[1][2]d[1][2]出现了x(y1)x∗(y−1)次……d[h][k]d[h][k] 出现了 (xh+1)(yk+1)(x−h+1)∗(y−k+1) 次。

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

i=1xj=1yd[i][j](x+1i)(y+1j)∑i=1x∑j=1yd[i][j]∗(x+1−i)∗(y+1−j)

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

(x+1)(y+1)i=1xj=1yd[i][j](x+1)∗(y+1)∗∑i=1x∑j=1yd[i][j]
(y+1)i=1xj=1yd[i][j]i−(y+1)∗∑i=1x∑j=1yd[i][j]∗i
(x+1)i=1xj=1yd[i][j]j−(x+1)∗∑i=1x∑j=1yd[i][j]∗j
+i=1xj=1yd[i][j]ij+∑i=1x∑j=1yd[i][j]∗i∗j

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

d[i][j],d[i][j]i,d[i][j]j,d[i][j]ijd[i][j],d[i][j]∗i,d[i][j]∗j,d[i][j]∗i∗j

这样就完成了!

#include <cstdio>
#include <cmath> #include <cstring> #include <algorithm> #include <iostream> using namespace std; typedef long long ll; ll read(){ char c; bool op = 0; while((c = getchar()) < '0' || c > '9') if(c == '-') op = 1; ll res = c - '0'; while((c = getchar()) >= '0' && c <= '9') res = res * 10 + c - '0'; return op ? -res : res; } const int N = 205; ll n, m, Q; ll t1[N][N], t2[N][N], t3[N][N], t4[N][N]; void add(ll x, ll y, ll z){ for(int X = x; X <= n; X += X & -X) for(int Y = y; Y <= m; Y += Y & -Y){ t1[X][Y] += z; t2[X][Y] += z * x; t3[X][Y] += z * y; t4[X][Y] += z * x * y; } } void range_add(ll xa, ll ya, ll xb, ll yb, ll z){ //(xa, ya) 到 (xb, yb) 的矩形 add(xa, ya, z); add(xa, yb + 1, -z); add(xb + 1, ya, -z); add(xb + 1, yb + 1, z); } ll ask(ll x, ll y){ ll res = 0; for(int i = x; i; i -= i & -i) for(int j = y; j; j -= j & -j) res += (x + 1) * (y + 1) * t1[i][j] - (y + 1) * t2[i][j] - (x + 1) * t3[i][j] + t4[i][j]; return res; } ll range_ask(ll xa, ll ya, ll xb, ll yb){ return ask(xb, yb) - ask(xb, ya - 1) - ask(xa - 1, yb) + ask(xa - 1, ya - 1); } int main(){ n = read(), m = read(), Q = read(); for(int i = 1; i <= n; i++){ for(int j = 1; j <= m; j++){ ll z = read(); range_add(i, j, i, j, z); } } while(Q--){ ll ya = read(), xa = read(), yb = read(), xb = read(), z = read(), a = read(); if(range_ask(xa, ya, xb, yb) < z * (xb - xa + 1) * (yb - ya + 1)) range_add(xa, ya, xb, yb, a); } for(int i = 1; i <= n; i++){ for(int j = 1; j <= m; j++) printf("%lld ", range_ask(i, j, i, j)); putchar('\n'); } return 0; }

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

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

1. 单点修改 + 区间查询

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

void add(int p, int x){ //给位置p增加x while(p <= n) sum[p] += x, p += p & -p; } int ask(int p){ //求位置p的前缀和 int res = 0; while(p) res += sum[p], p -= p & -p; return res; } int range_ask(int l, int r){ //区间求和 return ask(r) - ask(l - 1); }

2. 区间修改 + 单点查询

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

查询

设原数组为a[i]a[i], 设数组d[i]=a[i]a[i1](a[0]=0)d[i]=a[i]−a[i−1](a[0]=0),则 a[i]=ij=1d[j]a[i]=∑j=1id[j],可以通过求d[i]d[i]的前缀和查询。

修改

当给区间[l,r][l,r]加上x的时候,a[l]a[l] 与前一个元素 a[l1]a[l−1] 的差增加了xx,a[r+1]a[r+1] 与 a[r]a[r] 的差减少了xx。根据d[i]d[i]数组的定义,只需给d[l]d[l] 加上 xx, 给d[r+1]d[r+1] 减去 xx 即可。

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

3. 区间修改 + 区间查询

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

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

位置p的前缀和 =

i=1pa[i]=i=1pj=1id[j]∑i=1pa[i]=∑i=1p∑j=1id[j]

在等式最右侧的式子pi=1ij=1d[j]∑i=1p∑j=1id[j]中,d[1]d[1] 被用了pp次,d[2]d[2]被用了p1p−1次……那么我们可以写出:

位置p的前缀和 =

i=1pj=1id[j]=i=1pd[i](pi+1)=(p+1)i=1pd[i]i=1pd[i]i∑i=1p∑j=1id[j]=∑i=1pd[i]∗(p−i+1)=(p+1)∗∑i=1pd[i]−∑i=1pd[i]∗i

那么我们可以维护两个数组的前缀和:
一个数组是 sum1[i]=d[i]sum1[i]=d[i],
另一个数组是 sum2[i]=d[i]isum2[i]=d[i]∗i。

查询

位置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。

void add(ll p, ll x){
    for(int i = p; i <= n; i += i & -i) sum1[i] += x, sum2[i] += x * p; } void range_add(ll l, ll r, ll x){ add(l, x), add(r + 1, -x); } ll ask(ll p){ ll res = 0; for(int i = p; i; i -= i & -i) res += (p + 1) * sum1[i] - sum2[i]; return res; } ll range_ask(ll l, ll r){ return ask(r) - ask(l - 1); }

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

4. 二维树状数组

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

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

单点修改 + 区间查询

void add(int x, int y, int z){ //将点(x, y)加上z int memo_y = y; while(x <= n){ y = memo_y; while(y <= n) tree[x][y] += z, y += y & -y; x += x & -x; } } void ask(int x, int y){//求左上角为(1,1)右下角为(x,y) 的矩阵和 int res = 0, memo_y = y; while(x){ y = memo_y; while(y) res += tree[x][y], y -= y & -y; x -= x & -x; } }

区间修改 + 单点查询

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

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

二维前缀和:

sum[i][j]=sum[i1][j]+sum[i][j1]sum[i1][j1]+a[i][j]sum[i][j]=sum[i−1][j]+sum[i][j−1]−sum[i−1][j−1]+a[i][j]

那么我们可以令差分数组d[i][j]d[i][j] 表示 a[i][j]a[i][j] 与 a[i1][j]+a[i][j1]a[i1][j1]a[i−1][j]+a[i][j−1]−a[i−1][j−1] 的差。

例如下面这个矩阵

 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

那么我们开始写代码吧!

void add(int x, int y, int z){ int memo_y = y; while(x <= n){ y = memo_y; while(y <= n) tree[x][y] += z, y += y & -y; x += x & -x; } } void range_add(int xa, int ya, int xb, int yb, int z){ add(xa, ya, z); add(xa, yb + 1, -z); add(xb + 1, ya, -z); add(xb + 1, yb + 1, z); } void ask(int x, int y){ int res = 0, memo_y = y; while(x){ y = memo_y; while(y) res += tree[x][y], y -= y & -y; x -= x & -x; } }

区间修改 + 区间查询

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

i=1xj=1yk=1ih=1jd[h][k]∑i=1x∑j=1y∑k=1i∑h=1jd[h][k]
(d[h][k]为点(h, k)对应的“二维差分”(同上题))

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

首先,类比一维数组,统计一下每个d[h][k]d[h][k]出现过多少次。d[1][1]d[1][1]出现了xyx∗y次,d[1][2]d[1][2]出现了x(y1)x∗(y−1)次……d[h][k]d[h][k] 出现了 (xh+1)(yk+1)(x−h+1)∗(y−k+1) 次。

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

i=1xj=1yd[i][j](x+1i)(y+1j)∑i=1x∑j=1yd[i][j]∗(x+1−i)∗(y+1−j)

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

(x+1)(y+1)i=1xj=1yd[i][j](x+1)∗(y+1)∗∑i=1x∑j=1yd[i][j]
(y+1)i=1xj=1yd[i][j]i−(y+1)∗∑i=1x∑j=1yd[i][j]∗i
(x+1)i=1xj=1yd[i][j]j−(x+1)∗∑i=1x∑j=1yd[i][j]∗j
+i=1xj=1yd[i][j]ij+∑i=1x∑j=1yd[i][j]∗i∗j

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

d[i][j],d[i][j]i,d[i][j]j,d[i][j]ijd[i][j],d[i][j]∗i,d[i][j]∗j,d[i][j]∗i∗j

这样就完成了!

#include <cstdio>
#include <cmath> #include <cstring> #include <algorithm> #include <iostream> using namespace std; typedef long long ll; ll read(){ char c; bool op = 0; while((c = getchar()) < '0' || c > '9') if(c == '-') op = 1; ll res = c - '0'; while((c = getchar()) >= '0' && c <= '9') res = res * 10 + c - '0'; return op ? -res : res; } const int N = 205; ll n, m, Q; ll t1[N][N], t2[N][N], t3[N][N], t4[N][N]; void add(ll x, ll y, ll z){ for(int X = x; X <= n; X += X & -X) for(int Y = y; Y <= m; Y += Y & -Y){ t1[X][Y] += z; t2[X][Y] += z * x; t3[X][Y] += z * y; t4[X][Y] += z * x * y; } } void range_add(ll xa, ll ya, ll xb, ll yb, ll z){ //(xa, ya) 到 (xb, yb) 的矩形 add(xa, ya, z); add(xa, yb + 1, -z); add(xb + 1, ya, -z); add(xb + 1, yb + 1, z); } ll ask(ll x, ll y){ ll res = 0; for(int i = x; i; i -= i & -i) for(int j = y; j; j -= j & -j) res += (x + 1) * (y + 1) * t1[i][j] - (y + 1) * t2[i][j] - (x + 1) * t3[i][j] + t4[i][j]; return res; } ll range_ask(ll xa, ll ya, ll xb, ll yb){ return ask(xb, yb) - ask(xb, ya - 1) - ask(xa - 1, yb) + ask(xa - 1, ya - 1); } int main(){ n = read(), m = read(), Q = read(); for(int i = 1; i <= n; i++){ for(int j = 1; j <= m; j++){ ll z = read(); range_add(i, j, i, j, z); } } while(Q--){ ll ya = read(), xa = read(), yb = read(), xb = read(), z = read(), a = read(); if(range_ask(xa, ya, xb, yb) < z * (xb - xa + 1) * (yb - ya + 1)) range_add(xa, ya, xb, yb, a); } for(int i = 1; i <= n; i++){ for(int j = 1; j <= m; j++) printf("%lld ", range_ask(i, j, i, j)); putchar('\n'); } return 0; }

猜你喜欢

转载自www.cnblogs.com/ZJXXCN/p/12242478.html
今日推荐