差分 --算法竞赛专题解析(32)
本系列文章将于2021年整理出版。前驱教材:《算法竞赛入门到进阶》 清华大学出版社
网购:京东 当当 作者签名书:点我
有建议请加QQ 群:567554289
@
差分是一种处理数据的巧妙而简单的方法,它应用于区间的修改和询问问题。把给定的数据元素集A分成很多区间,对这些区间做很多次操作,每次操作是对某个区间内的所有元素做相同的加减操作,若一个个地修改这个区间内的每个元素,非常耗时。引入“差分数组”D,当修改某个区间时,只需要修改这个区间的“端点”,就能记录整个区间的修改,而对端点的修改非常容易,是\(O(1)\)复杂度的。当所有的修改操作结束后,再利用差分数组,计算出新的A。
数据A可以是一维的线性数组\(a[]\)、二维矩阵\(a[][]\)、三维立体\(a[][][]\)。相应地,定义差分数组\(D[]、D[][]、D[][][]\)。一维差分很容易理解,二维和三维需要一点想象力。
1. 一维差分
1.1 一维差分的概念
讨论这样一个场景:
(1)给定一个长度为n的一维数组\(a[]\),数组内每个元素有初始值。
(2)修改操作:做m次区间修改,每次修改对区间内所有元素做相同的加减操作。例如第\(i\)次修改,把区间\([Li, Ri]\)内所有元素加上\(di\)。
(3)询问操作:询问一个元素的新值是多少。
如果简单地用暴力法编码,那么每次修改的复杂度是\(O(n)\)的,m次修改共\(O(mn)\),总复杂度\(O(mn)\),效率很差。利用差分法,可以把复杂度减少到\(O(m+n)\)。
在差分法中,用到了两个数组:原数组\(a[]\)、差分数组\(D[]\)。
差分数组D[]的定义是\(D[k] = a[k] - a[k-1]\),即原数组\(a[]\)的相邻元素的差。从定义可以推出\(a[k] = D[1] + D[2] + ... + D[k]\) ,也就是说,\(a[]\)是\(D[]\)的前缀和。这个公式揭示了\(a[]\)和\(D[]\)的关系,“差分是前缀和的逆运算”,它把求\(a[k]\)转化为求D的前缀和。为加深对前缀和的理解,可以把每个\(D[]\)看成一条直线上的小线段,它的两端是相邻的\(a[]\);这些小线段相加,就得到了从起点开始的长线段\(a[]\)。
注意,\(a[]\)和\(D[]\)的值都可能为负,下面图中所有的\(D[]\)都是长度为正的线段,只是为了方便图示。
如何用差分数组记录区间修改?为什么利用差分数组能提升修改的效率呢?
把区间\([L, R]\)内每个元素加上\(d\),对应的\(D[]\)做以下操作:
(1)把\(D[L]\)加上\(d\):
D[L] += d
(2)把\(D[R+1]\)减去\(d\):
D[R+1] -= d
每次操作只需要修改区间\([L, R]\)的两个端点的\(D[]\)值,复杂度是\(O(1)\)的。经过这种操作后,原来直接在\(a[]\)上做的复杂度为\(O(n)\)的区间修改操作,就变成了在\(D[]\)上做的复杂度为\(O(1)\)的端点操作。
利用\(D[]\),能精确地实现只修改区间内元素的目的,而不会修改区间外的\(a[]\)值。因为前缀和\(a[x] = D[1] + D[2] + ... + D[x]\),有:
(1)\(1 ≤ x < L\),前缀和\(a[x]\)不变;
(2)\(L ≤ x ≤ R\),前缀和\(a[x]\)增加了\(d\);
(3)\(R < x ≤ N\),前缀和\(a[x]\)不变,因为被\(D[R+1]\)中减去的\(d\)抵消了。
完成区间修改并得到\(D[]\)后,最后用\(D[]\)计算\(a[]\),复杂度是\(O(n)\)的。m次区间修改和1次查询,总复杂度为\(O(m + n)\),比暴力法的\(O(mn)\)好多了。
下面给出一个例题。
Color the ball hdu 1556 http://acm.hdu.edu.cn/showproblem.php?pid=1556
问题描述:N个气球排成一排,从左到右依次编号为1, 2, 3 .... N。每次给定2个整数L, R(L<= R),lele从气球L开始到气球R依次给每个气球涂一次颜色。但是N次以后lele已经忘记了第I个气球已经涂过几次颜色了,你能帮他算出每个气球被涂过几次颜色吗?
输入:每个测试实例第一行为一个整数N,(N <= 100000)。接下来的N行,每行包括2个整数L, R(1 <= L<= R<= N)。当N = 0,输入结束。
输出:每个测试实例输出一行,包括N个整数,第I个数代表第I个气球总共被涂色的次数。
这个例题是简单差分法的直接应用,下面给出代码。代码第13、14行是区间修改,第17行的\(a[i] = a[i-1] + D[i]\),即利用\(D[]\)求得了最后的\(a[]\)。这个式子就是\(a[i] - a[i-1] = D[i]\),它是差分数组的定义。
注意\(a[]\)的计算方法。\(a[i] = a[i-1] + D[i]\)是一个递推公式,通过它能在一个\(i\)循环中求得所有的\(a[]\)。如果不用递推,而是直接用前缀和\(a[k]=D[1] + D[2] + ... + D[k]\) 来求所有的\(a[]\),就需要用两个循环\(i、k\)。
//hdu 1556用差分数组求解
#include<bits/stdc++.h>
using namespace std;
const int Maxn = 100010;
int a[Maxn],D[Maxn]; //a是气球,D是差分数组
int main(){
int n;
while(~scanf("%d",&n)) {
memset(a,0,sizeof(a)); memset(D,0,sizeof(D));
for(int i=1;i<=n;i++){
int L,R; scanf("%d%d",&L,&R);
D[L]++; //区间修改,这里d=1
D[R+1]--;
}
//小技巧:17行到20行,把a[]改成D[]也行
for(int i=1;i<=n;i++){ //求原数组
a[i] = a[i-1] + D[i]; //差分。求前缀和a[],a[i]就是气球i的值
if(i!=n) printf("%d ", a[i]); //逐个打印结果
else printf("%d\n",a[i]);
}
}
return 0;
}
上面的代码用了一个小技巧,可以省掉\(a[]\),从而节省空间。在17行后求原数组\(a[]\)的时候,在推导式子\(a[i] = a[i-1] + D[i]\)时,把已经使用过的较小的\(D[]\)直接当成\(a []\)即可。把第17~20行的\(a[]改为D[]\),也能通过。这个技巧在后面的二维差分、三维差分中也能用,节省一倍的空间。
1.2 差分的局限性
读者已经注意到,利用差分数组\(D[]\)可以把\(O(n)\)的区间修改,变成\(O(1)\)的端点修改,从而提高了修改操作的效率。
但是,一次查询操作,即查询某个\(a[i]\),需要用\(D[]\)计算整个原数组\(a[]\),计算量是\(O(n)\)的,即一次查询的复杂度是\(O(n)\)的。在上面的例题中,如果查询不是发生了一次,而是这样:有m次修改,有k次查询,且修改和查询的顺序是随机的。此时总复杂度是:m次修改复杂度\(O(m)\),k次查询复杂度\(O(kn)\),总复杂度\(O(m + kn)\)。还不如直接用暴力法,总复杂度\(O(mn + k)\)。
这种题型是“区间修改+单点查询”,用差分数组往往不够用。因为差分数组对“区间修改”很高效,但是对“单点查询”并不高效。此时需要用树状数组和线段树来求解,详情见第4章的树状数组、线段树专题。在树状数组专题中,重新讲解了hdu 1556这道例题。
树状数组常常结合差分数组来解决更复杂的问题,见本博客的树状数组专题。差分数组也常用于“树上差分”,见本博客LCA专题的“树上差分”。
2. 二维差分
从一维差分容易扩展到二维差分。一维是线性数组,一个区间\([L, R]\)有两个端点;二维是矩阵,一个区间由四个端点围成。
下面给出一个模板题。
地毯 洛谷P3397 https://www.luogu.com.cn/problem/P3397
问题描述:在 n×n 的格子上有m个地毯。给出这些地毯的信息,问每个点被多少个地毯覆盖。
输入: 第一行是两个正整数n,m。接下来m行,每行2个坐标(x1, y1)和(x2, y2),代表一块地毯,左上角是(x1, y1),右下角是(x2, y2)。
输出: 输出n行,每行n个正整数。第i行第j列的正整数表示(i, j)这个格式被多少地毯覆盖。
这一题是hdu 1556的二维扩展,其修改操作和查询操作完全一样。
存储矩阵需要很大的空间。如果题目有空间限制,例如100M,那么二维差分能处理多大的n?定义两个二维矩阵\(a[][]和D[][]\),设矩阵的每个元素是2字节的\(int\)型,可以计算出最大的n = 5000。不过,也可以不定义\(a[][]\),而是像一维情况下一样,直接用\(D[][]来表示a[][]\),这样能剩下一半的空间。
在用差分之前,先考虑能不能用暴力法。每次修改复杂度是\(O(n^2)\),共m次,总复杂度\(O(m×n^2)\),超时。
二维差分的复杂度是多少?一维差分的一次修改是\(O(1)\)的,二维差分的修改估计也是\(O(1)\)的;一维差分的一次查询是\(O(n)\)的,二维差分是\(O(n^2)\)的,所以二维差分的总复杂度是\(O(m + n^2)\)。由于计算一次二维矩阵的值需要\(O(n^2)\)次计算,所以二维差分已经达到了最好的复杂度。
下面从一维差分推广到二维差分。
(1)前缀和。
在一维差分中,原数组\(a[]\)是从第1个\(D[1]\)开始的差分数组\(D[]\)的前缀和:\(a[k] = D[1] + D[2] + ... + D[k]\)。
在二维差分中,\(a[][]\)是差分数组\(D[][]\)的前缀和,即由原点坐标\((1, 1)\)和坐标\((i, j)\)围成的矩阵中,所有的\(D[][]\)相加等于\(a[i][j]\)。为加深对前缀和的理解,可以把每个\(D[][]\)看成一个小格;在坐标\((1, 1)和(i, j)\)所围成的范围内,所有小格子加起来的总面积,等于\(a[i][j]\)。下面的图中,每个格子的面积是一个\(D[][]\),例如阴影格子是\(D[i][j]\),它由4个坐标点定义:\((i-1, j)、(i, j)、(i-1, j-1)、(i, j-1)\)。坐标点\((i, j)\)的值是\(a[i][j]\),它等于坐标\((1, 1)和(i, j)\)所围成的所有格子的总面积。图中故意把小格子画得长宽不同,是为了体现它们的面积不同。
注意在一些题目中,\(D[][]\)可以为负。图中把\(D[][]\)用“面积”来演示,而面积都是正的,这个图示只是为了加深对前缀和的理解。
(2)差分的定义。在一维情况下,\(D[i] = a[i] - a[i-1]\)。在二维情况下,差分变成了相邻的\(a[][]\)的“面积差”,计算公式是:\(D[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]\)被减了2次,所以需要加回来1次。
(3)区间修改。在一维情况下,做区间修改只需要修改区间的两个端点的\(D[]\)值。在二维情况下,一个区间是一个小矩阵,有4个端点,只需要修改这4个端点的\(D[][]\)值。例如坐标点\((x1, y1)\) ~ \((x2, y2)\)定义的区间,对应4个端点的\(D[][]\):
D[x1][y1] += d; //二维区间的起点
D[x1][y2+1] -= d; //把x看成常数,y从y1到y2+1
D[x2+1][y1] -= d; //把y看成常数,x从x1到x2+1
D[x2+1][y2+1] += d; //由于前两式把d减了2次,多减了1次,这里加1次回来
下图是区间修改的图示。2个黑色点围成的矩形是题目给出的区间修改范围。只需要改变4个\(D[][]\)值,即改变图中的4个阴影块的面积。读者可以用这个图,观察每个坐标点的\(a[][]\)值的变化情况。例如符号“∆”标记的坐标\((x2+1, y2)\),它在修改的区间之外;\(a[x2+1][y2]\)的值是从\((1,1)到(x2+1, y2)\)的总面积,在这个范围内,\(D[x1][y1]+d,D[x2+1][y1]-d\),两个\(d\)抵消,\(a[x2+1][y2]\)保持不变。
下面给出洛谷P3397的两种实现。
2.1 用差分数组的递推公式求前缀和
前缀和\(a[][]\)的计算用到了递推公式:
\(a[i][j] = D[i][j] + a[i-1][j] + a[i][j-1] - a[i-1][j-1];\)
16行到23行用\(D[][]\)推出\(a[][]\)并打印出来。
为了节约空间,可以不定义\(a[][]\),而是把用过的\(D[][]\)看成\(a[][]\)。这个小技巧在一维差分中介绍过。
#include<bits/stdc++.h>
using namespace std;
int D[5000][5000]; //差分数组
//int a[5000][5000]; //原数组,不定义也行
int main(){
int n,m;
scanf("%d%d",&n,&m);
while(m--){
int x1,y1,x2,y2;
scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
D[x1][y1] += 1; //计算差分数组
D[x2+1][y1] -= 1;
D[x1][y2+1] -= 1;
D[x2+1][y2+1] += 1;
}
for(int i=1;i<=n;++i){ //根据差分数组计算原矩阵的值(想象成求小格子的面积和)
for(int j=1;j<=n;++j){ //把用过的D[][]看成a[][],就不用再定义a[][]了
//a[i][j] = D[i][j] + a[i-1][j] + a[i][j-1] - a[i-1][j-1];
//printf("%d ",a[i][j]); //这两行和下面两行的效果一样
D[i][j] += D[i-1][j]+D[i][j-1]-D[i-1][j-1];
printf("%d ",D[i][j]);
}
printf("\n");//换行
}
return 0;
}
2.2 直接计算前缀和
其实不用递推公式,而是直接求前缀和也行。根据图2,前缀和是总面积,分别从\(x\)方向和\(y\)方向,用两次循环计算,并直接用\(D[][]\)记录结果,最后算出的\(D[][]\)就是\(a[][]\)。
以阴影处的\(D[2][2]\)为例,它最后的值代表\(a[2][2]\),是4个小格子的总面积:
\(D[1][1] + D[1][2] + D[2][1] + D[2][2]\)
计算过程是:
(1)先累加计算\(y\)方向,得:
\(D[1][2] = D[1][1]+ D[1][2]、D[2][2] = D[2][1]+ D[2][2]\)
(2)再累加计算\(x\)方向,得:
\(D[2][1]=D[1][1]+D[2][1]、D[2][2]=D[1][2]+D[2][2]= D[1][1]+D[1][2]+ D[2][1]+ D[2][2]\)
实际上,在这个计算过程中,\(D[1][1]、D[1][2]、D[2][1]、D[2][2]\)都更新了,计算结果代表了\(a[1][1]、a[1][2]、a[2][1]、a[2][2]\)。
把方法1代码的16-24行替换为下面的代码,最后得到的\(D[][]\)就是所有的前缀和,即最新的\(a[][]\)。请对照图2理解代码。
for(int i=1; i<=n; ++i)
for(int j=1; j<n; ++j) //注意这里是j<n
D[i][j+1] += D[i][j]; //把i看成定值,先累加计算j方向
for(int j=1; j<=n; ++j)
for(int i=1; i<n; ++i) //注意这里是i<n
D[i+1][j] += D[i][j]; //把j看成定值,再累加计算i方向
for(int i=1; i<=n; ++i) { //打印
for(int j=1; j<=n; ++j)
printf("%d ",D[i][j]);
printf("\n"); //换行
}
对比这两种代码:
(1)这两种代码的复杂度是一样的。从计算量上看,没有优劣之分。
(2)代码2不如代码1清晰简洁,所以代码2这种写法一般也用不着。
(3)代码2也有优点,它不需要用到递推公式,而是直接求前缀和。
这里给出代码2这种方法,是为了在下一小节的三维差分中使用它。由于在三维情况下,差分数组的\(D[][][]\)和原数组\(a[][][]\)的递推公式很难写出来,所以用代码2这种方法更容易编码。
3. 三维差分
三维差分的模板代码比较少见。
三维差分比较复杂,请结合本节中的几何图进行理解。
与一维差分、二维差分的思路类似,下面给出三维差分的有关特性。
(1)元素的值用三维数组\(a[][][]\)来定义,差分数组\(D[][][]\)也是三维的。把三维差分想象成在立体空间上的操作。一维的区间是一个线段,二维是矩形,那么三维就是立体块。一个小立体块有8个顶点,所以三维的区间修改,需要修改8个\(D[][][]\)值。
(2)前缀和。
在二维差分中,\(a[][]\)是差分数组\(D[][]\)的前缀和,即由原点坐标\((1, 1)\)和坐标\((i, j)\)围成的矩阵中,所有的\(D[][]\)(看成小格子)相加等于\(a[i][j]\)(看成总面积)。
在三维差分中,\(a[][][]\)是差分数组\(D[][][]\)的前缀和。即由原点坐标\((1, 1, 1)\)和坐标\((i, j, k)\)所标记的范围中,所有的\(D[][][]\)相加等于\(a[i][j][k]\)。把每个\(D[][][]\)看成一个小立方体;在坐标\((1, 1, 1)\)和\((i, j, k)\)所围成的空间中,所有小立体块加起来的总体积,等于\(a[i][j][k]\)。每个小立方体由8个坐标点定义,见下面图中的坐标点。坐标点\((i, j, k)\)的值是\(a[i][j][k]\);\(D[i][j][k]\)的值是图中小立方体的体积。
(3)差分的定义。在三维情况下,差分变成了相邻的\(a[][][]\)的“体积差”。如何写出差分的递推计算公式?
一维差分和二维差分的递推计算公式很好写。
三维差分,\(D[i][j][k]\)的几何意义是图中小立方体的体积,它可以通过这个小立方体的8个顶点的值推出来。思路与二维情况下类似,二维的\(D[][]\)是通过小矩形的四个顶点的\(a[][]\)值来计算的。不过,三维情况下,递推计算公式很难写,8个顶点有8个\(a[][][]\),把脑袋绕晕了也不容易写对。
上一小节的二维差分中,曾用过另一种方法,直接对D数组求前缀和。在三维情况下也可以用这种方法求前缀和,得到所有的\(a[][][]\)的最新值。
(4)区间修改。在三维情况下,一个区间是一个立方体,有8个顶点,只需要修改这8个顶点的\(D[][][]\)值。例如坐标点\((x1, y1, z1)\) ~\((x2, y2, z2)\)定义的区间,对应8个\(D[][][]\),请对照上面的图来想象它们的位置。
D[x1][y1][z1] += d; //前面:左下顶点,即区间的起始点
D[x2+1][y1][z1] -= d; //前面:右下顶点的右边一个点
D[x1][y1][z2+1] -= d; //前面:左上顶点的上面一个点
D[x2+1][y1][z2+1] += d; //前面:右上顶点的斜右上方一个点
D[x1][y2+1][z1] -= d; //后面:左下顶点的后面一个点
D[x2+1][y2+1][z1] += d; //后面:右下顶点的斜右后方一个点
D[x1][y2+1][z2+1] += d; //后面:左上顶点的斜后上方一个点
D[x2+1][y2+1][z2+1] -= d; //后面:右上顶点的斜右上后方一个点,即区间终点的后一个点
下面给出一个三维差分的例题。
三体攻击 蓝桥杯2018年省赛A组
提交地址:https://www.lanqiao.cn/problems/180/learning/
问题描述:三体人将对地球发起攻击。为了抵御攻击,地球人派出了n = A × B × C 艘战舰,在太空中排成一个 A 层 B 行 C 列的立方体。其中,第 i 层第 j 行第 k 列的战舰(记为战舰 (i, j, k))的生命值为 s(i, j, k)。
三体人将会对地球发起 m 轮“立方体攻击”,每次攻击会对一个小立方体中的所有战舰都造成相同的伤害。具体地,第 t 轮攻击用 7 个参数 x1, x2, y1, y2, z1, z2, d 描述;
所有满足i∈[x1, x2], j∈[y1, y2], k∈[z1, z2] 的战舰 (i, j, k) 会受到 d 的伤害。如果一个战舰累计受到的总伤害超过其防御力,那么这个战舰会爆炸。
地球指挥官希望你能告诉他,第一艘爆炸的战舰是在哪一轮攻击后爆炸的。
输入:第一行包括 4 个正整数 A, B, C, m;
第二行包含 A × B × C 个整数,其中第 ((i − 1)×B + (j − 1)) × C + (k − 1)+1 个数为 s(i, j, k);
第 3 到第 m + 2 行中,第 (t − 2) 行包含 7 个正整数 x1, x2, y1, y2, z1, z2, d。
A × B × C ≤ 10^6, m ≤ 10^6, 0 ≤ s(i, j, k), d ≤ 10^9。
输出:输出第一个爆炸的战舰是在哪一轮攻击后爆炸的。保证一定存在这样的战舰。
首先看数据规模,有\(n=10^6\)个点, \(m=10^6\)次攻击,如果用暴力法,统计每次攻击后每个点的生命值,那么复杂度是\(O(mn)\)的,超时。
本题适合用三维差分,每次攻击只修改差分数组\(D[][][]\),一次修改的复杂度是\(O(1)\),\(m\)次修改的总复杂度只有\(O(m)\)。
但是光用差分数组并不能解决问题。因为在差分数组上查询区间内的每个元素是否小于0,需要用差分数组来计算区间内每个元素的值,复杂度是\(O(n)\)的。合起来的总复杂度还是O(mn)的,跟暴力法的复杂度一样。
本题需要结合第二个算法:二分法。从第1次修改到第m次修改,肯定有一次修改是临界点。在临界点前,没有负值(战舰爆炸);在临界点后,出现了负值,且后面一直有负值。那么对m进行二分,就能在\(O(logm)\)次内找到这个临界点,这就是答案。总复杂度\(O(nlogm)\)。
下面给出代码。其中check()
函数包含了三维差分的全部内容。代码有几个关键点:
(1)没有定义\(a[][][]\),而是用\(D[][][]\)来代替。
(2)压维。直接定义三维差分数组\(D[][][]\)不太方便。虽然坐标点总数量\(n = A × B × C = 10^6\)比较小,但是每一维都需要定义到\(10^6\),那么总空间就是\(10^{18}\)。为避免这一问题,可以把三维坐标压维成一维数组\(D[]\),总长度仍然是\(10^6\)的。这个技巧很有用。实现函数是num()
,它把三维坐标\((x, y, z)\)变换为一维坐标\(h = (x-1)*B*C + (y-1)*C + (z-1) + 1\),当\(x、y、z\)的取值范围分别是1 ~ A、1 ~ B、1 ~ C时,\(h\)的范围是1 ~ A × B × C。
如果希望按C语言的习惯从0开始,\(x、y、z\)的取值范围分别是0 ~ A-1、0 ~ B-1、0 ~ C-1,h范围是0 ~ A × B × C-1,就把式子改为:\(h = x*B*C + y*C + z\)。
同理,二维坐标\((x, y)\)也可以压维成一维\(h = (x-1)*B + (y-1) + 1\),当\(x、y\)的取值范围分别是1 ~ A、1 ~ B时,\(h\)的范围是1 ~ A × B。
(3)check()
中19-26行,在\(D[]\)上记录区间修改。
(4)check()
中29-40行的3个for
循环计算前缀和,原理见二维差分的代码2。它分别从\(x、y、z\)三个方向累加小立方体的体积,计算出所有的前缀和。
#include<stdio.h>
int A,B,C,n,m;
const int Maxn = 1000005;
int s[Maxn]; //存储舰队生命值
int D[Maxn]; //三维差分数组(压维);同时也用来计算每个点的攻击值
int x2[Maxn], y2[Maxn], z2[Maxn]; //存储区间修改的范围,即攻击的范围
int x1[Maxn], y1[Maxn], z1[Maxn];
int d[Maxn]; //记录伤害,就是区间修改
int num(int x,int y,int z) {
//小技巧:压维,把三维坐标[(x,y,z)转为一维的((x-1)*B+(y-1))*C+(z-1)+1
if (x>A || y>B || z>C) return 0;
return ((x-1)*B+(y-1))*C+(z-1)+1;
}
bool check(int x){ //做x次区间修改。即检查经过x次攻击后是否有战舰爆炸
for (int i=1; i<=n; i++) D[i]=0; //差分数组的初值,本题是0
for (int i=1; i<=x; i++) { //用三维差分数组记录区间修改:有8个区间端点
D[num(x1[i], y1[i], z1[i])] += d[i];
D[num(x2[i]+1,y1[i], z1[i])] -= d[i];
D[num(x1[i], y1[i], z2[i]+1)] -= d[i];
D[num(x2[i]+1,y1[i], z2[i]+1)] += d[i];
D[num(x1[i], y2[i]+1,z1[i])] -= d[i];
D[num(x2[i]+1,y2[i]+1,z1[i])] += d[i];
D[num(x1[i], y2[i]+1,z2[i]+1)] += d[i];
D[num(x2[i]+1,y2[i]+1,z2[i]+1)] -= d[i];
}
//下面从x、y、z三个方向计算前缀和
for (int i=1; i<=A; i++)
for (int j=1; j<=B; j++)
for (int k=1; k<C; k++) //把x、y看成定值,累加z方向
D[num(i,j,k+1)] += D[num(i,j,k)];
for (int i=1; i<=A; i++)
for (int k=1; k<=C; k++)
for (int j=1; j<B; j++) //把x、z看成定值,累加y方向
D[num(i,j+1,k)] += D[num(i,j,k)];
for (int j=1; j<=B; j++)
for (int k=1; k<=C; k++)
for (int i=1; i<A; i++) //把y、z看成定值,累加x方向
D[num(i+1,j,k)] += D[num(i,j,k)];
for (int i=1; i<=n; i++) //最后判断是否攻击值大于生命值
if (D[i]>s[i])
return true;
return false;
}
int main() {
scanf("%d%d%d%d", &A, &B, &C, &m);
n = A*B*C;
for (int i=1; i<=n; i++) scanf("%d", &s[i]); //读生命值
for (int i=1; i<=m; i++) //读每次攻击的范围,用坐标表示
scanf("%d%d%d%d%d%d%d",&x1[i],&x2[i],&y1[i],&y2[i],&z1[i],&z2[i],&d[i]);
int L = 1,R = m; //经典的二分写法
while (L<R) { //对m进行二分,找到临界值。总共只循环了log(m)次
int mid = (L+R)>>1;
if (check(mid)) R = mid;
else L = mid+1;
}
printf("%d\n", R); //打印临界值
return 0;
}
4. 差分习题
一维差分:poj 3263;hdu 6273,1121;洛谷P3406,P3948,P4552
二维差分:洛谷P3397,hdu 6514
三维差分:蓝桥杯A组2018省赛“三体攻击”