luogu-P4514-二维树状数组

fmj_123

2018-06-07 12:09:32

Solution

Update 20191114 修了一些bug - 题目大意:有一个写满0的矩阵,要求有 ``` 1. L a b c d delta 代表将 (a,b),(c,d)(a,b),(c,d) 为顶点的矩形区域内的所有数字加上delta。 2. k a b c d 代表求 (a,b),(c,d)(a,b),(c,d) 为顶点的矩形区域内所有数字的和。 ``` 这两种操作 - 总结:区间修改,区间查询 OK ,看到这dalao们的反应都是线段树或树状数组了,因为~~听说线段树过不去~~,所以我用了树状数组。 --- ### 树状数组怎么用?如何区间修改,单点查询?请参考 ## [3374](/problemnew/show/P3374) ## [3368](/problemnew/show/P3368) --- 我们从一维树状数组的“区间修改,区间查询“开始 使用差分的方法,差分数组$d_i$为第$i$个数($a_i$)与第$i-1$($a_{i-1}$)个数的差,那么 因为 $a_i=d_1+d_2+...+d_{i-1}+d_i$ ### 所以 $d_i$实际上用了$n-(i-1)$次,相当于有$i-1$次没被使用 所以 我们可以建两个树状数组, 第一个(本部分称为$tree$)存$d_i$ 第二个(本部分称为$tree1$)存$d_i*(i-1)$ #### 求1~x的总和就是 $tree$中x的前缀和*x-$tree1$中x的前缀和 对于此公式,我的思路是先把x前面的数看成$a_x$,再减去多算的值($tree1$数组) 贴代码(P3372) ``` // luogu-judger-enable-o2 //本代码中,tree和tree1的意义与前文相同 #include<bits/stdc++.h> using namespace std; long long n,tree[101000],tree1[100000],m,c,l,r,k; void add(int x,int y) { //修改tree for (int i=x;i<=n;i+=(i&(-i))) tree[i]+=y; } void add1(int x,int y) { //修改tree1 for (int i=x;i<=n;i+=(i&(-i))) tree1[i]+=y; } long long ask(long long x) { long long ans=0,ans1=0; for (long long i=x;i;i-=(i&(-i))) ans+=tree[i]; ans*=x; for (long long i=x;i;i-=(i&(-i))) ans1+=tree1[i]; //分别求出ans*x与ans1(应该可以合起来写) return ans-ans1; } int main() { scanf("%lld%lld",&n,&m); long long c=0,last=0,a=0; for (long long i=1;i<=n;i++) { scanf("%lld",&a);//long long读入要用lld c=a-last;//求差分 add(i,c);add1(i,c*(i-1)); last=a; } for (long long i=1;i<=m;i++) { scanf("%lld",&c); if (c==1) { //区间修改 scanf("%lld%lld%lld",&l,&r,&k); add(l,k);add(r+1,-k);//在l处+x add1(l,k*(l-1));add1(r+1,(-k)*(r));//在r+1处-x,相当于抵消前面的+x } if (c==2) { //区间查询 scanf("%d%d",&l,&r); cout<<ask(r)-ask(l-1)<<endl; } } return 0; } ``` --- 回归正题,二维树状数组的区间修改,区间查询怎么做呢? (二维树状数组的定义可以baidu一下) 因为差分实际上是前缀和的逆运算,所以,仿照二维前缀和($s[i][j]=a[i][j]+a[i-1][j]+a[i][j-1]-a[i-1][j-1]$),我们可以写出二维差分公式 $d[i][j]=a[i][j]-a[i-1][j]-a[i][j-1]+a[i-1][j-1]$ 修改就是这样 ``` //原数组 0 x x x 0 x x x 0 x x x ``` ``` //差分数组 0 +x 0 0 -x 0 0 0 0 0 0 0 0 0 0 0 -x 0 0 +x ``` 仿照一维的“区间修改,区间查询”我们可以得出 ### $d[i][j]$实际上被使用了$(n-i+1)(m-j+1)$次,相当于有$j(i-1)+i(j-1)+(i-1)(j-1)$次没被使用 因此,我们可以维护4个树状数组 第一个(本部分称为$tree$)存$d[i][j]$ 第二个(本部分称为$tree1$)存$d[i][j]*(i-1)$ 第三个(本部分称为$tree2$)存$d[i][j]*(j-1)$ 第二个(本部分称为$tree3$)存$d[i][j]*(i-1)*(j-1)$ 最后,求$(1,1)$到$(i,j)$的前缀和就是 #### $tree[i][j]$的前缀和$*x*y$-$tree1[i][j]$的前缀和$*j$-$tree2[i][j]$的前缀和$*i$+$tree3[i][j]$的前缀和 (这个公式和普通的前缀和差不多啊好像) 我的思路和前面一样,先把所有数看成$a[i][j]$,再减差值 最后贴本题代码 ``` // luogu-judger-enable-o2 //本代码中,tree、tree1、tree2、tree3的意义与前文相同 //码风不好求轻喷 #include<bits/stdc++.h> using namespace std; long long tree[2049][2049],tree1[2049][2049],tree2[2049][2049],tree3[2049][2049]; long long n,m,a,b,c,d,ad;char ch; void add(long long x,long long y,long long z) { for (long long i=x;i<=n;i+=i&(-i)) { for (long long j=y;j<=m;j+=(j&(-j))) { tree[i][j]+=z; tree1[i][j]+=z*(x-1); tree2[i][j]+=z*(y-1); tree3[i][j]+=z*(x-1)*(y-1);//分别修改四个数组 } } } void fa(long long a,long long b,long long c,long long d,long long ad) { //分别修改四个点 add(a,b,ad);add(c+1,b,-ad); add(a,d+1,-ad);add(c+1,d+1,ad); } long long ask(long long x,long long y) { long long ans=0,ans1=0,ans2=0,ans3=0; for (long long i=x;i;i-=(i&(-i))) { for (long long j=y;j;j-=(j&(-j))) { ans+=tree[i][j]; ans1+=tree1[i][j]; ans2+=tree2[i][j]; ans3+=tree3[i][j]; } } ans*=x*y; //分别求和 return ans-ans1*y-ans2*x+ans3; } int main() { cin>>ch; scanf("%d%lld",&n,&m); while (cin>>ch) { if (ch=='L') { //修改 scanf("%lld%lld%lld%lld%lld",&a,&b,&c,&d,&ad); fa(a,b,c,d,ad); } if (ch=='k') { //查询 scanf("%lld%lld%lld%lld",&a,&b,&c,&d); cout<<ask(c,d)-ask(c,b-1)-ask(a-1,d)+ask(a-1,b-1)<<endl; } } return 0; } ``` 本文部分参考自[胡小兔的博客](https://www.cnblogs.com/RabbitHu/p/BIT.html),并感谢ta的博客对我学习此方法的帮助