本系列文章将于2021年整理出版。前驱教材:《算法竞赛入门到进阶》 清华大学出版社
网购:京东 当当   作者签名书:点我
有建议请加QQ 群:567554289

文章目录

  • 1. 一维差分
    • 1.1 一维差分的概念
    • 1.2 差分的局限性
  • 2. 二维差分
    • 2.1 用差分数组的递推公式求前缀和
    • 2.2 直接计算前缀和
  • 3. 三维差分
  • 4. 差分习题

   差分是一种处理数据的巧妙而简单的方法,它应用于区间的修改和询问问题。把给定的数据元素集A分成很多区间,对这些区间做很多次操作,每次操作是对某个区间内的所有元素做相同的加减操作,若一个个地修改这个区间内的每个元素,非常耗时。引入“差分数组”D,当修改某个区间时,只需要修改这个区间的“端点”,就能记录整个区间的修改,而对端点的修改非常容易,是O(1)O(1)O(1)复杂度的。当所有的修改操作结束后,再利用差分数组,计算出新的A。
  数据A可以是一维的线性数组a[]a[]a[]、二维矩阵a[][]a[][]a[][]、三维立体a[][][]a[][][]a[][][]。相应地,定义差分数组D[]、D[][]、D[][][]D[]、D[][]、D[][][]D[]、D[][]、D[][][]。一维差分很容易理解,二维和三维需要一点想象力。

1. 一维差分

1.1 一维差分的概念

   讨论这样一个场景:
   (1)给定一个长度为n的一维数组a[]a[]a[],数组内每个元素有初始值。
   (2)修改操作:做m次区间修改,每次修改对区间内所有元素做相同的加减操作。例如第iii次修改,把区间[Li,Ri][Li, Ri][Li,Ri]内所有元素加上dididi。
   (3)询问操作:询问一个元素的新值是多少。
   如果简单地用暴力法编码,那么每次修改的复杂度是O(n)O(n)O(n)的,m次修改共O(mn)O(mn)O(mn),总复杂度O(mn)O(mn)O(mn),效率很差。利用差分法,可以把复杂度减少到O(m+n)O(m+n)O(m+n)。
   在差分法中,用到了两个数组:原数组a[]a[]a[]、差分数组D[]D[]D[]。
   差分数组D[]的定义是D[k]=a[k]−a[k−1]D[k] = a[k] - a[k-1]D[k]=a[k]−a[k−1],即原数组a[]a[]a[]的相邻元素的差。从定义可以推出a[k]=D[1]+D[2]+...+D[k]a[k] = D[1] + D[2] + ... + D[k]a[k]=D[1]+D[2]+...+D[k] ,也就是说,a[]a[]a[]是D[]D[]D[]的前缀和。这个公式揭示了a[]a[]a[]和D[]D[]D[]的关系,“差分是前缀和的逆运算”,它把求a[k]a[k]a[k]转化为求D的前缀和。为加深对前缀和的理解,可以把每个D[]D[]D[]看成一条直线上的小线段,它的两端是相邻的a[]a[]a[];这些小线段相加,就得到了从起点开始的长线段a[]a[]a[]。
   注意,a[]a[]a[]和D[]D[]D[]的值都可能为负,下面图中所有的D[]D[]D[]都是长度为正的线段,只是为了方便图示。

图1 把每个D[]看成小线段,把每个a[]看成从a[1]开始的小线段的和

  
  如何用差分数组记录区间修改?为什么利用差分数组能提升修改的效率呢?
  把区间[L,R][L, R][L,R]内每个元素加上ddd,对应的D[]D[]D[]做以下操作:
  (1)把D[L]D[L]D[L]加上ddd:

     D[L] += d

  (2)把D[R+1]D[R+1]D[R+1]减去ddd:

     D[R+1] -= d

  每次操作只需要修改区间[L,R][L, R][L,R]的两个端点的D[]D[]D[]值,复杂度是O(1)O(1)O(1)的。经过这种操作后,原来直接在a[]a[]a[]上做的复杂度为O(n)O(n)O(n)的区间修改操作,就变成了在D[]D[]D[]上做的复杂度为O(1)O(1)O(1)的端点操作。
  利用D[]D[]D[],能精确地实现只修改区间内元素的目的,而不会修改区间外的a[]a[]a[]值。因为前缀和a[x]=D[1]+D[2]+...+D[x]a[x] = D[1] + D[2] + ... + D[x]a[x]=D[1]+D[2]+...+D[x],有:
  (1)1≤x<L1 ≤ x < L1≤x<L,前缀和a[x]a[x]a[x]不变;
  (2)L≤x≤RL ≤ x ≤ RL≤x≤R,前缀和a[x]a[x]a[x]增加了ddd;
  (3)R<x≤NR < x ≤ NR<x≤N,前缀和a[x]a[x]a[x]不变,因为被D[R+1]D[R+1]D[R+1]中减去的ddd抵消了。
  完成区间修改并得到D[]D[]D[]后,最后用D[]D[]D[]计算a[]a[]a[],复杂度是O(n)O(n)O(n)的。m次区间修改和1次查询,总复杂度为O(m+n)O(m + n)O(m+n),比暴力法的O(mn)O(mn)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]a[i] = a[i-1] + D[i]a[i]=a[i−1]+D[i],即利用D[]D[]D[]求得了最后的a[]a[]a[]。这个式子就是a[i]−a[i−1]=D[i]a[i] - a[i-1] = D[i]a[i]−a[i−1]=D[i],它是差分数组的定义。
  注意a[]a[]a[]的计算方法。a[i]=a[i−1]+D[i]a[i] = a[i-1] + D[i]a[i]=a[i−1]+D[i]是一个递推公式,通过它能在一个iii循环中求得所有的a[]a[]a[]。如果不用递推,而是直接用前缀和a[k]=D[1]+D[2]+...+D[k]a[k]=D[1] + D[2] + ... + D[k]a[k]=D[1]+D[2]+...+D[k] 来求所有的a[]a[]a[],就需要用两个循环i、ki、ki、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=1D[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[]a[]a[],从而节省空间。在17行后求原数组a[]a[]a[]的时候,在推导式子a[i]=a[i−1]+D[i]a[i] = a[i-1] + D[i]a[i]=a[i−1]+D[i]时,把已经使用过的较小的D[]D[]D[]直接当成a[]a []a[]即可。把第17~20行的a[]改为D[]a[]改为D[]a[]改为D[],也能通过。这个技巧在后面的二维差分、三维差分中也能用,节省一倍的空间。

1.2 差分的局限性

  读者已经注意到,利用差分数组D[]D[]D[]可以把O(n)O(n)O(n)的区间修改,变成O(1)O(1)O(1)的端点修改,从而提高了修改操作的效率。
  但是,一次查询操作,即查询某个a[i]a[i]a[i],需要用D[]D[]D[]计算整个原数组a[]a[]a[],计算量是O(n)O(n)O(n)的,即一次查询的复杂度是O(n)O(n)O(n)的。在上面的例题中,如果查询不是发生了一次,而是这样:有m次修改,有k次查询,且修改和查询的顺序是随机的。此时总复杂度是:m次修改复杂度O(m)O(m)O(m),k次查询复杂度O(kn)O(kn)O(kn),总复杂度O(m+kn)O(m + kn)O(m+kn)。还不如直接用暴力法,总复杂度O(mn+k)O(mn + k)O(mn+k)。
  这种题型是“区间修改+单点查询”,用差分数组往往不够用。因为差分数组对“区间修改”很高效,但是对“单点查询”并不高效。此时需要用树状数组和线段树来求解,详情见第4章的树状数组、线段树专题。在树状数组专题中,重新讲解了hdu 1556这道例题。
  树状数组常常结合差分数组来解决更复杂的问题,见本博客的树状数组专题。差分数组也常用于“树上差分”,见本博客LCA专题的“树上差分”。

2. 二维差分

  从一维差分容易扩展到二维差分。一维是线性数组,一个区间[L,R][L, R][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[][]a[][]和D[][]a[][]和D[][],设矩阵的每个元素是2字节的intintint型,可以计算出最大的n = 5000。不过,也可以不定义a[][]a[][]a[][],而是像一维情况下一样,直接用D[][]来表示a[][]D[][]来表示a[][]D[][]来表示a[][],这样能剩下一半的空间。
  在用差分之前,先考虑能不能用暴力法。每次修改复杂度是O(n2)O(n^2)O(n2),共m次,总复杂度O(m×n2)O(m×n^2)O(m×n2),超时。
  二维差分的复杂度是多少?一维差分的一次修改是O(1)O(1)O(1)的,二维差分的修改估计也是O(1)O(1)O(1)的;一维差分的一次查询是O(n)O(n)O(n)的,二维差分是O(n2)O(n^2)O(n2)的,所以二维差分的总复杂度是O(m+n2)O(m + n^2)O(m+n2)。由于计算一次二维矩阵的值需要O(n2)O(n^2)O(n2)次计算,所以二维差分已经达到了最好的复杂度。
  下面从一维差分推广到二维差分。
  (1)前缀和。
  在一维差分中,原数组a[]a[]a[]是从第1个D[1]D[1]D[1]开始的差分数组D[]D[]D[]的前缀和:a[k]=D[1]+D[2]+...+D[k]a[k] = D[1] + D[2] + ... + D[k]a[k]=D[1]+D[2]+...+D[k]。
  在二维差分中,a[][]a[][]a[][]是差分数组D[][]D[][]D[][]的前缀和,即由原点坐标(1,1)(1, 1)(1,1)和坐标(i,j)(i, j)(i,j)围成的矩阵中,所有的D[][]D[][]D[][]相加等于a[i][j]a[i][j]a[i][j]。为加深对前缀和的理解,可以把每个D[][]D[][]D[][]看成一个小格;在坐标(1,1)和(i,j)(1, 1)和(i, j)(1,1)和(i,j)所围成的范围内,所有小格子加起来的总面积,等于a[i][j]a[i][j]a[i][j]。下面的图中,每个格子的面积是一个D[][]D[][]D[][],例如阴影格子是D[i][j]D[i][j]D[i][j],它由4个坐标点定义:(i−1,j)、(i,j)、(i−1,j−1)、(i,j−1)(i-1, j)、(i, j)、(i-1, j-1)、(i, j-1)(i−1,j)、(i,j)、(i−1,j−1)、(i,j−1)。坐标点(i,j)(i, j)(i,j)的值是a[i][j]a[i][j]a[i][j],它等于坐标(1,1)和(i,j)(1, 1)和(i, j)(1,1)和(i,j)所围成的所有格子的总面积。图中故意把小格子画得长宽不同,是为了体现它们的面积不同。

图2 把每个a[][]看成总面积,把每个D[][]看成小格子的面积

  
  注意在一些题目中,D[][]D[][]D[][]可以为负。图中把D[][]D[][]D[][]用“面积”来演示,而面积都是正的,这个图示只是为了加深对前缀和的理解。
  (2)差分的定义。在一维情况下,D[i]=a[i]−a[i−1]D[i] = a[i] - a[i-1]D[i]=a[i]−a[i−1]。在二维情况下,差分变成了相邻的a[][]a[][]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]D[i][j]=a[i][j]–a[i−1][j]–a[i][j−1]+a[i−1][j−1]。这个公式可以通过上面的图来观察。阴影方格表示D[i][j]D[i][j]D[i][j]的值,它的面积这样求:大面积a[i][j]a[i][j]a[i][j]减去两个小面积a[i−1][j]、a[i][j−1]a[i-1][j]、a[i][j-1]a[i−1][j]、a[i][j−1],由于两个小面积的公共面积a[i−1][j−1]a[i-1][j-1]a[i−1][j−1]被减了2次,所以需要加回来1次。
  (3)区间修改。在一维情况下,做区间修改只需要修改区间的两个端点的D[]D[]D[]值。在二维情况下,一个区间是一个小矩阵,有4个端点,只需要修改这4个端点的D[][]D[][]D[][]值。例如坐标点(x1,y1)(x1, y1)(x1,y1) ~ (x2,y2)(x2, y2)(x2,y2)定义的区间,对应4个端点的D[][]D[][]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[][]D[][]D[][]值,即改变图中的4个阴影块的面积。读者可以用这个图,观察每个坐标点的a[][]a[][]a[][]值的变化情况。例如符号“∆”标记的坐标(x2+1,y2)(x2+1, y2)(x2+1,y2),它在修改的区间之外;a[x2+1][y2]a[x2+1][y2]a[x2+1][y2]的值是从(1,1)到(x2+1,y2)(1,1)到(x2+1, y2)(1,1)到(x2+1,y2)的总面积,在这个范围内,D[x1][y1]+d,D[x2+1][y1]−dD[x1][y1]+d,D[x2+1][y1]-dD[x1][y1]+d,D[x2+1][y1]−d,两个ddd抵消,a[x2+1][y2]a[x2+1][y2]a[x2+1][y2]保持不变。

图3 二维差分的区间修改

  下面给出洛谷P3397的两种实现。

2.1 用差分数组的递推公式求前缀和

  前缀和a[][]a[][]a[][]的计算用到了递推公式:
    a[i][j]=D[i][j]+a[i−1][j]+a[i][j−1]−a[i−1][j−1];a[i][j] = D[i][j] + a[i-1][j] + a[i][j-1] - a[i-1][j-1];a[i][j]=D[i][j]+a[i−1][j]+a[i][j−1]−a[i−1][j−1];
  16行到23行用D[][]D[][]D[][]推出a[][]a[][]a[][]并打印出来。
  为了节约空间,可以不定义a[][]a[][]a[][],而是把用过的D[][]D[][]D[][]看成a[][]a[][]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,前缀和是总面积,分别从xxx方向和yyy方向,用两次循环计算,并直接用D[][]D[][]D[][]记录结果,最后算出的D[][]D[][]D[][]就是a[][]a[][]a[][]。

图4 在D[][]上计算前缀和

  以阴影处的D[2][2]D[2][2]D[2][2]为例,它最后的值代表a[2][2]a[2][2]a[2][2],是4个小格子的总面积:
    D[1][1]+D[1][2]+D[2][1]+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]
  计算过程是:
  (1)先累加计算yyy方向,得:
    D[1][2]=D[1][1]+D[1][2]、D[2][2]=D[2][1]+D[2][2]D[1][2] = D[1][1]+ D[1][2]、D[2][2] = D[2][1]+ D[2][2]D[1][2]=D[1][1]+D[1][2]、D[2][2]=D[2][1]+D[2][2]
  (2)再累加计算xxx方向,得:
    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[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[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]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]a[1][1]、a[1][2]、a[2][1]、a[2][2]a[1][1]、a[1][2]、a[2][1]、a[2][2]。
  把方法1代码的16-24行替换为下面的代码,最后得到的D[][]D[][]D[][]就是所有的前缀和,即最新的a[][]a[][]a[][]。请对照图2理解代码。

    for(int i=1; i<=n; ++i)           for(int j=1; j<n; ++j)        //注意这里是j<nD[i][j+1] += D[i][j];     //把i看成定值,先累加计算j方向for(int j=1; j<=n; ++j)for(int i=1; i<n; ++i)        //注意这里是i<nD[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[][][]D[][][]D[][][]和原数组a[][][]a[][][]a[][][]的递推公式很难写出来,所以用代码2这种方法更容易编码。

3. 三维差分

  三维差分的模板代码比较少见。
  三维差分比较复杂,请结合本节中的几何图进行理解。
  与一维差分、二维差分的思路类似,下面给出三维差分的有关特性。
  (1)元素的值用三维数组a[][][]a[][][]a[][][]来定义,差分数组D[][][]D[][][]D[][][]也是三维的。把三维差分想象成在立体空间上的操作。一维的区间是一个线段,二维是矩形,那么三维就是立体块。一个小立体块有8个顶点,所以三维的区间修改,需要修改8个D[][][]D[][][]D[][][]值。
  (2)前缀和。
  在二维差分中,a[][]a[][]a[][]是差分数组D[][]D[][]D[][]的前缀和,即由原点坐标(1,1)(1, 1)(1,1)和坐标(i,j)(i, j)(i,j)围成的矩阵中,所有的D[][]D[][]D[][](看成小格子)相加等于a[i][j]a[i][j]a[i][j](看成总面积)。
  在三维差分中,a[][][]a[][][]a[][][]是差分数组D[][][]D[][][]D[][][]的前缀和。即由原点坐标(1,1,1)(1, 1, 1)(1,1,1)和坐标(i,j,k)(i, j, k)(i,j,k)所标记的范围中,所有的D[][][]D[][][]D[][][]相加等于a[i][j][k]a[i][j][k]a[i][j][k]。把每个D[][][]D[][][]D[][][]看成一个小立方体;在坐标(1,1,1)(1, 1, 1)(1,1,1)和(i,j,k)(i, j, k)(i,j,k)所围成的空间中,所有小立体块加起来的总体积,等于a[i][j][k]a[i][j][k]a[i][j][k]。每个小立方体由8个坐标点定义,见下面图中的坐标点。坐标点(i,j,k)(i, j, k)(i,j,k)的值是a[i][j][k]a[i][j][k]a[i][j][k];D[i][j][k]D[i][j][k]D[i][j][k]的值是图中小立方体的体积。

图5立体的坐标

  (3)差分的定义。在三维情况下,差分变成了相邻的a[][][]a[][][]a[][][]的“体积差”。如何写出差分的递推计算公式?
  一维差分和二维差分的递推计算公式很好写。
  三维差分,D[i][j][k]D[i][j][k]D[i][j][k]的几何意义是图中小立方体的体积,它可以通过这个小立方体的8个顶点的值推出来。思路与二维情况下类似,二维的D[][]D[][]D[][]是通过小矩形的四个顶点的a[][]a[][]a[][]值来计算的。不过,三维情况下,递推计算公式很难写,8个顶点有8个a[][][]a[][][]a[][][],把脑袋绕晕了也不容易写对。
上一小节的二维差分中,曾用过另一种方法,直接对D数组求前缀和。在三维情况下也可以用这种方法求前缀和,得到所有的a[][][]a[][][]a[][][]的最新值。
  (4)区间修改。在三维情况下,一个区间是一个立方体,有8个顶点,只需要修改这8个顶点的D[][][]D[][][]D[][][]值。例如坐标点(x1,y1,z1)(x1, y1, z1)(x1,y1,z1) ~(x2,y2,z2)(x2, y2, z2)(x2,y2,z2)定义的区间,对应8个D[][][]D[][][]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=106n=10^6n=106个点, m=106m=10^6m=106次攻击,如果用暴力法,统计每次攻击后每个点的生命值,那么复杂度是O(mn)O(mn)O(mn)的,超时。
  本题适合用三维差分,每次攻击只修改差分数组D[][][]D[][][]D[][][],一次修改的复杂度是O(1)O(1)O(1),mmm次修改的总复杂度只有O(m)O(m)O(m)。
  但是光用差分数组并不能解决问题。因为在差分数组上查询区间内的每个元素是否小于0,需要用差分数组来计算区间内每个元素的值,复杂度是O(n)O(n)O(n)的。合起来的总复杂度还是O(mn)的,跟暴力法的复杂度一样。
  本题需要结合第二个算法:二分法。从第1次修改到第m次修改,肯定有一次修改是临界点。在临界点前,没有负值(战舰爆炸);在临界点后,出现了负值,且后面一直有负值。那么对m进行二分,就能在O(logm)O(logm)O(logm)次内找到这个临界点,这就是答案。总复杂度O(nlogm)O(nlogm)O(nlogm)。
下面给出代码。其中check()函数包含了三维差分的全部内容。代码有几个关键点:
  (1)没有定义a[][][]a[][][]a[][][],而是用D[][][]D[][][]D[][][]来代替。
  (2)压维。直接定义三维差分数组D[][][]D[][][]D[][][]不太方便。虽然坐标点总数量n=A×B×C=106n = A × B × C = 10^6n=A×B×C=106比较小,但是每一维都需要定义到10610^6106,那么总空间就是101810^{18}1018。为避免这一问题,可以把三维坐标压维成一维数组D[]D[]D[],总长度仍然是10610^6106的。这个技巧很有用。实现函数是num(),它把三维坐标(x,y,z)(x, y, z)(x,y,z)变换为一维坐标h=(x−1)∗B∗C+(y−1)∗C+(z−1)+1h = (x-1)*B*C + (y-1)*C + (z-1) + 1h=(x−1)∗B∗C+(y−1)∗C+(z−1)+1,当x、y、zx、y、zx、y、z的取值范围分别是1 ~ A、1 ~ B、1 ~ C时,hhh的范围是1 ~ A × B × C。
  如果希望按C语言的习惯从0开始,x、y、zx、y、zx、y、z的取值范围分别是0 ~ A-1、0 ~ B-1、0 ~ C-1,h范围是0 ~ A × B × C-1,就把式子改为:h=x∗B∗C+y∗C+zh = x*B*C + y*C + zh=x∗B∗C+y∗C+z。
  同理,二维坐标(x,y)(x, y)(x,y)也可以压维成一维h=(x−1)∗B+(y−1)+1h = (x-1)*B + (y-1) + 1h=(x−1)∗B+(y−1)+1,当x、yx、yx、y的取值范围分别是1 ~ A、1 ~ B时,hhh的范围是1 ~ A × B。
  (3)check()中19-26行,在D[]D[]D[]上记录区间修改。
  (4)check()中29-40行的3个for循环计算前缀和,原理见二维差分的代码2。它分别从x、y、zx、y、zx、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)+1if (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;  //差分数组的初值,本题是0for (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省赛“三体攻击”

差分 --算法竞赛专题解析(32)相关推荐

  1. 尺取法 --算法竞赛专题解析(2)

    本系列文章将于2021年整理出版,书名<算法竞赛专题解析>. 前驱教材:<算法竞赛入门到进阶> 清华大学出版社 2019.8 网购:京东 当当      作者签名书 如有建议, ...

  2. 二分法、三分法 --算法竞赛专题解析(1)

    本系列文章将于2021年整理出版,书名<算法竞赛专题解析>. 前驱教材:<算法竞赛入门到进阶> 清华大学出版社 2019.8 网购:京东 当当      作者签名书 如有建议, ...

  3. 四边形不等式优化 --算法竞赛专题解析(10)

    本系列文章将于2021年整理出版,书名<算法竞赛专题解析>. 前驱教材:<算法竞赛入门到进阶> 清华大学出版社 2019.8 网购:京东 当当      作者签名书 如有建议, ...

  4. 树形DP --算法竞赛专题解析(17)

    本系列文章将于2021年整理出版,书名<算法竞赛专题解析>. 前驱教材:<算法竞赛入门到进阶> 清华大学出版社 网购:京东 当当      想要一本作者签名书?点我 如有建议, ...

  5. 线性丢番图方程 --算法竞赛专题解析(21):数论

    本系列文章将于2021年整理出版.前驱教材:<算法竞赛入门到进阶> 清华大学出版社 网购:京东 当当   作者签名书:点我 公众号同步:算法专辑    暑假福利:胡说三国 有建议请加QQ ...

  6. 同余 --算法竞赛专题解析(22):数论

    本系列文章将于2021年整理出版.前驱教材:<算法竞赛入门到进阶> 清华大学出版社 网购:京东 当当   作者签名书:点我 公众号同步:算法专辑    暑假福利:胡说三国 有建议请加QQ ...

  7. 线段树 --算法竞赛专题解析(24)

    本系列文章将于2021年整理出版.前驱教材:<算法竞赛入门到进阶> 清华大学出版社 网购:京东 当当   作者签名书:点我 有建议请加QQ 群:567554289 文章目录 1. 线段树概 ...

  8. a*算法迷宫 c++_算法竞赛专题解析(12):搜索基础

    搜索 搜索,就是查找解空间,它是"暴力法"算法思想的具体实现. 文章目录: 01 搜索简介 02 搜索算法的基本思路 03 BFS的性质和代码实现 04 DFS的常见操作和代码实现 ...

  9. 二维差分算法最细致解析

    解析前提:大家比较明白了一维差分算法和前缀和算法 大家要注意一些编程思维: 首先编程基本运算就是迭代,迭代思维是不考虑边界问题,更重要的是将计算的问题一般化,抽象化,在过程中考虑变量身份.其次就是互逆 ...

  10. 算法竞赛入门经典 习题3-2 分子量 Molar Mass

    给出一种物质的分子式(不带括号),求其分子量.本题分子式中只包含四种原子,分别为C.H.O.N,原子量分别为12.01,1.008,16.00,14.01.例如,C6H5OH的分子量为94.108g/ ...

最新文章

  1. observeOn()与subscribeOn()的详解
  2. OpenGL之渲染大小球自转和公转的效果
  3. php连接mysql并读取数据
  4. 使用Azure DevOps持续集成GitHub项目
  5. jquery ajax html编码,jQuery AJAX字符编码
  6. (数据库系统概论|王珊)第二章关系数据库-第二节、第三节:关系操作和关系完整性
  7. c语言综合知识,软件设计师教程综合知识集锦之C语言编程风格
  8. 华为管理学案例分析_管理学论文5000字如何高质量写作
  9. java后端开发简历模板,最全Java知识总结
  10. 电容或电感的电压_电感电容等储能元件
  11. 大胜凭德--入行选领导(转载分析)
  12. 用计算机计算勾股定理,勾股定理电脑计算器
  13. 面向对象继承 C#编程记录
  14. Oracle特殊符号
  15. html修改li大小,css为li设置不同宽度
  16. 微信小程序 一键授权 给第三方平台代开发管理(二,一键授权给第三方平台)
  17. 测试用例设计方法(一)
  18. 批量转换 gbk 文件到utf8 文件,不引用三方类库【 java 代码】
  19. ecshop被加入了黑链
  20. 判断并求出两个圆的交点(平面几何)

热门文章

  1. python123货币转换器_python货币转换
  2. App、H5、PC应用多端开发框架Flutter 2发布
  3. Screeps入门: harvest,upgrader,builder初级自动化
  4. 人人商城图片错乱问题
  5. 数据库复习(四)Redis
  6. 最后一天购书优惠!好书必买,不容错过!
  7. 搜索关键词优化 助力全网霸屏营销
  8. Learn Go with tests 学习笔记(9)——Mocking
  9. 时间记录APP———Time Meter
  10. Mars3D开发基础学习:三维场景 Map