主頁 > 軟體設計 > 差分 --演算法競賽專題決議(32)

差分 --演算法競賽專題決議(32)

2021-02-11 13:04:40 軟體設計

本系列文章將于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次區間修改,每次修改對區間內所有元素做相同的加減操作,例如第 i i i次修改,把區間 [ L i , R i ] [Li, Ri] [Li,Ri]內所有元素加上 d i di di
?? (3)詢問操作:詢問一個元素的新值是多少,
?? 如果簡單地用暴力法編碼,那么每次修改的復雜度是 O ( n ) O(n) O(n)的,m次修改共 O ( m n ) O(mn) O(mn),總復雜度 O ( m n ) 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]內每個元素加上 d d d,對應的 D [ ] D[] D[]做以下操作:
??(1)把 D [ L ] D[L] D[L]加上 d d d

     D[L] += d

??(2)把 D [ R + 1 ] D[R+1] D[R+1]減去 d d d

     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 < L 1 ≤ x < L 1x<L,前綴和 a [ x ] a[x] a[x]不變;
??(2) L ≤ x ≤ R L ≤ x ≤ R LxR,前綴和 a [ x ] a[x] a[x]增加了 d d d
??(3) R < x ≤ N R < x ≤ N R<xN,前綴和 a [ x ] a[x] a[x]不變,因為被 D [ R + 1 ] D[R+1] D[R+1]中減去的 d d d抵消了,
??完成區間修改并得到 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 ( m n ) 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]是一個遞推公式,通過它能在一個 i i i回圈中求得所有的 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 、 k i、k ik

//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 [ ] 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 ( k n ) O(kn) O(kn),總復雜度 O ( m + k n ) O(m + kn) O(m+kn),還不如直接用暴力法,總復雜度 O ( m n + 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位元組的 i n t int int型,可以計算出最大的n = 5000,不過,也可以不定義 a [ ] [ ] a[][] a[][],而是像一維情況下一樣,直接用 D [ ] [ ] 來 表 示 a [ ] [ ] D[][]來表示a[][] D[][]a[][],這樣能剩下一半的空間,
??在用差分之前,先考慮能不能用暴力法,每次修改復雜度是 O ( n 2 ) O(n^2) O(n2),共m次,總復雜度 O ( m × n 2 ) 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 ( n 2 ) O(n^2) O(n2)的,所以二維差分的總復雜度是 O ( m + n 2 ) O(m + n^2) O(m+n2),由于計算一次二維矩陣的值需要 O ( n 2 ) 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[][]值,例如坐標點 ( x 1 , y 1 ) (x1, y1) (x1,y1) ~ ( x 2 , y 2 ) (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[][]值的變化情況,例如符號“?”標記的坐標 ( x 2 + 1 , y 2 ) (x2+1, y2) (x2+1,y2),它在修改的區間之外; a [ x 2 + 1 ] [ y 2 ] a[x2+1][y2] a[x2+1][y2]的值是從 ( 1 , 1 ) 到 ( x 2 + 1 , y 2 ) (1,1)到(x2+1, y2) (1,1)(x2+1,y2)的總面積,在這個范圍內, D [ x 1 ] [ y 1 ] + d , D [ x 2 + 1 ] [ y 1 ] ? d D[x1][y1]+d,D[x2+1][y1]-d D[x1][y1]+dD[x2+1][y1]?d,兩個 d d d抵消, a [ x 2 + 1 ] [ y 2 ] 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,前綴和是總面積,分別從 x x x方向和 y y y方向,用兩次回圈計算,并直接用 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)先累加計算 y y y方向,得:
???? 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)再累加計算 x x 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[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<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 [ ] [ ] [ ] 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[][][]值,例如坐標點 ( x 1 , y 1 , z 1 ) (x1, y1, z1) (x1,y1,z1) ~ ( x 2 , y 2 , z 2 ) (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 = 1 0 6 n=10^6 n=106個點, m = 1 0 6 m=10^6 m=106次攻擊,如果用暴力法,統計每次攻擊后每個點的生命值,那么復雜度是 O ( m n ) O(mn) O(mn)的,超時,
??本題適合用三維差分,每次攻擊只修改差分陣列 D [ ] [ ] [ ] D[][][] D[][][],一次修改的復雜度是 O ( 1 ) O(1) O(1) m m m次修改的總復雜度只有 O ( m ) O(m) O(m)
??但是光用差分陣列并不能解決問題,因為在差分陣列上查詢區間內的每個元素是否小于0,需要用差分陣列來計算區間內每個元素的值,復雜度是 O ( n ) O(n) O(n)的,合起來的總復雜度還是O(mn)的,跟暴力法的復雜度一樣,
??本題需要結合第二個演算法:二分法,從第1次修改到第m次修改,肯定有一次修改是臨界點,在臨界點前,沒有負值(戰艦爆炸);在臨界點后,出現了負值,且后面一直有負值,那么對m進行二分,就能在 O ( l o g m ) O(logm) O(logm)次內找到這個臨界點,這就是答案,總復雜度 O ( n l o g m ) O(nlogm) O(nlogm)
下面給出代碼,其中check()函式包含了三維差分的全部內容,代碼有幾個關鍵點:
??(1)沒有定義 a [ ] [ ] [ ] a[][][] a[][][],而是用 D [ ] [ ] [ ] D[][][] D[][][]來代替,
??(2)壓維,直接定義三維差分陣列 D [ ] [ ] [ ] D[][][] D[][][]不太方便,雖然坐標點總數量 n = A × B × C = 1 0 6 n = A × B × C = 10^6 n=A×B×C=106比較小,但是每一維都需要定義到 1 0 6 10^6 106,那么總空間就是 1 0 1 8 10^18 1018,經過壓維,用一維陣列 D [ ] D[] D[],總長度仍然是 1 0 6 10^6 106的,這個技巧很有用,
??(3)check()中19-26行,在 D [ ] D[] D[]上記錄區間修改,
??(4)check()中29-40行的3個for回圈計算前綴和,原理見二維差分的代碼2,它分別從 x 、 y 、 z x、y、z xyz三個方向累加小立方體的體積,計算出所有的前綴和,

#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省賽“三體攻擊”

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/258781.html

標籤:其他

上一篇:已經消失的智能指標——auto_ptr

下一篇:深入理解分布式技術 - 分布式呼叫跟蹤

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more