• 差分 --算法竞赛专题解析(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[])都是长度为正的线段,只是为了方便图示。

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

      
      如何用差分数组记录区间修改?为什么利用差分数组能提升修改的效率呢?
      把区间([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
    ",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))所围成的所有格子的总面积。图中故意把小格子画得长宽不同,是为了体现它们的面积不同。

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

      
      注意在一些题目中,(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])保持不变。

    图3 二维差分的区间修改

      下面给出洛谷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("
    ");//换行
        }
        return 0;
    }
    

    2.2 直接计算前缀和

      其实不用递推公式,而是直接求前缀和也行。根据图2,前缀和是总面积,分别从(x)方向和(y)方向,用两次循环计算,并直接用(D[][])记录结果,最后算出的(D[][])就是(a[][])

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

      以阴影处的(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("
    ");                 //换行
        }
    

      对比这两种代码:
      (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])的值是图中小立方体的体积。

    图5立体的坐标

      (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
    ", R);  //打印临界值
        return 0;
    }
    

    4. 差分习题

    一维差分:poj 3263;hdu 6273,1121;洛谷P3406,P3948,P4552
    二维差分:洛谷P3397,hdu 6514
    三维差分:蓝桥杯A组2018省赛“三体攻击”

  • 相关阅读:
    398. Random Pick Index
    739. Daily Temperatures
    779. K-th Symbol in Grammar
    698. Partition to K Equal Sum Subsets
    783. Minimum Distance Between BST Nodes
    asp.netcore 深入了解配置文件加载过程
    啥叫K8s?啥是k8s?
    Asp.NetCore轻松学-实现一个轻量级高可复用的RabbitMQ客户端
    Asp.Net Core 轻松学-一行代码搞定文件上传
    目录---Asp.NETCore轻松学系列【目录】
  • 原文地址:https://www.cnblogs.com/luoyj/p/14403414.html
Copyright © 2020-2023  润新知