【資料結構與演算法】字首和與差分

gonghr 發表於 2021-09-04
演算法 資料結構

字首和

一維字首和

字首和陣列 sum 的每一位記錄的是當前位置距離起點位置,這連續一段的和區間和。

利用字首和陣列,我們可以快速得到陣列任意區間的元素和。

構造字首和陣列的時間複雜度是O(n),獲得區間和的複雜度是O(1)

image

當nums陣列的元素下標從0開始算時,需要做出一些調整

image

模板和例題

LeetCode 303. 區域和檢索 - 陣列不可變

給定一個整數陣列  nums,求出陣列從索引 i 到 j(i ≤ j)範圍內元素的總和,包含 i、j 兩點。

實現 NumArray 類:

NumArray(int[] nums) 使用陣列 nums 初始化物件
int sumRange(int i, int j) 返回陣列 nums 從索引 i 到 j(i ≤ j)範圍內元素的總和,包含 i、j 兩點(也就是 sum(nums[i], nums[i + 1], ... , nums[j]))
 

示例:

輸入:
["NumArray", "sumRange", "sumRange", "sumRange"]
[[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]]
輸出:
[null, 1, -1, -3]

解釋:
NumArray numArray = new NumArray([-2, 0, 3, -5, 2, -1]);
numArray.sumRange(0, 2); // return 1 ((-2) + 0 + 3)
numArray.sumRange(2, 5); // return -1 (3 + (-5) + 2 + (-1)) 
numArray.sumRange(0, 5); // return -3 ((-2) + 0 + 3 + (-5) + 2 + (-1))
 

提示:

0 <= nums.length <= 104
-105 <= nums[i] <= 105
0 <= i <= j < nums.length
最多呼叫 104 次 sumRange 方法

class NumArray {
    int[] sum;
    public NumArray(int[] nums) {
        int n = nums.length;
        // 字首和陣列下標從 1 開始,因此設定長度為 n + 1(模板部分)
        sum = new int[n + 1];
        // 預處理除字首和陣列(模板部分)
        for (int i = 1; i <= n; i++) 
            sum[i] = sum[i - 1] + nums[i - 1];
    }
    public int sumRange(int i, int j) {
        // 求某一段區域和 [i, j] 的模板是 sum[j] - sum[i - 1](模板部分)
        // 但由於我們原陣列下標從 0 開始,因此要在模板的基礎上進行 + 1
        i++; j++;
        return sum[j] - sum[i - 1];
    }
}

二維字首和

用來解決二維矩陣中的矩形區域求和問題

sum矩陣中每一個數(x, y)表示(0, 0)為左上角到(x, y)為右下角的矩形數值總和

image

模板和例題

// 預處理字首和陣列
{
    sum = new int[n + 1][m + 1];
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            // 當前格子(和) = 上方的格子(和) + 左邊的格子(和) - 左上角的格子(和) + 當前格子(值)【和是指對應的字首和,值是指原陣列中的值】
            sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + matrix[i - 1][j - 1];
        }
    }
}

// 首先我們要令左上角為 (x1, y1) 右下角為 (x2, y2)
// 計算 (x1, y1, x2, y2) 的結果
{
    // 字首和是從 1 開始,原陣列是從 0 開始,上來先將原陣列座標全部 +1,轉換為字首和座標
    x1++; y1++; x2++; y2++;
    // 記作 22 - 12 - 21 + 11,然後 不減,減第一位,減第二位,減兩位
    // 也可以記作 22 - 12(x - 1) - 21(y - 1) + 11(x y 都 - 1)
    ans = sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1];
}

LeetCode 304. 二維區域和檢索 - 矩陣不可變

image

給定一個二維矩陣 matrix,以下型別的多個請求:

計算其子矩形範圍內元素的總和,該子矩陣的左上角為 (row1, col1) ,右下角為 (row2, col2) 。
實現 NumMatrix 類:

NumMatrix(int[][] matrix) 給定整數矩陣 matrix 進行初始化
int sumRegion(int row1, int col1, int row2, int col2) 返回左上角 (row1, col1) 、右下角 (row2, col2) 的子矩陣的元素總和。
 

示例 1:

輸入: 
["NumMatrix","sumRegion","sumRegion","sumRegion"]
[[[[3,0,1,4,2],[5,6,3,2,1],[1,2,0,1,5],[4,1,0,1,7],[1,0,3,0,5]]],[2,1,4,3],[1,1,2,2],[1,2,2,4]]
輸出: 
[null, 8, 11, 12]

解釋:
NumMatrix numMatrix = new NumMatrix([[3,0,1,4,2],[5,6,3,2,1],[1,2,0,1,5],[4,1,0,1,7],[1,0,3,0,5]]]);
numMatrix.sumRegion(2, 1, 4, 3); // return 8 (紅色矩形框的元素總和)
numMatrix.sumRegion(1, 1, 2, 2); // return 11 (綠色矩形框的元素總和)
numMatrix.sumRegion(1, 2, 2, 4); // return 12 (藍色矩形框的元素總和)
 

提示:

m == matrix.length
n == matrix[i].length
1 <= m, n <= 200
-105 <= matrix[i][j] <= 105
0 <= row1 <= row2 < m
0 <= col1 <= col2 < n
最多呼叫 104 次 sumRegion 方法

class NumMatrix {
    int[][] sum;
    public NumMatrix(int[][] matrix) {
        int n = matrix.length, m = n == 0 ? 0 : matrix[0].length;
        // 與「一維字首和」一樣,字首和陣列下標從 1 開始,因此設定矩陣形狀為 [n + 1][m + 1](模板部分)
        sum = new int[n + 1][m + 1];
        // 預處理除字首和陣列(模板部分)
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + matrix[i - 1][j - 1];
            }
        }
    }

    public int sumRegion(int x1, int y1, int x2, int y2) {
        // 求某一段區域和 [i, j] 的模板是 sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1];(模板部分)
        // 但由於我們源陣列下標從 0 開始,因此要在模板的基礎上進行 + 1
        x1++; y1++; x2++; y2++;
        return sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1];
    }
}

差分

一維差分

「差分」是求「字首和」的逆向過程。

但是差分陣列的構造過程並不重要,利用差分的性質實現區間修改和單點查詢是重點

差分陣列的求法:c[i] = nums[i] - nums[i-1],原陣列中相鄰元素的差值組成的陣列就是差分陣列

image

差分陣列對應的概念是字首和陣列,差分陣列的第 i 個數即為原陣列的第 i-1 個元素和第 i 個元素的差值,也就是說我們對差分陣列求字首和即可得到原陣列

差分陣列的性質是,當我們希望對原陣列的某一個區間 [l,r] 施加一個增量inc 時,差分陣列 d 對應的改變是:d[l] 增加 incd[r+1] 減少 inc。這樣對於區間的修改就變為了對於兩個位置的修改。並且這種修改是可以疊加的,即當我們多次對原陣列的不同區間施加不同的增量,我們只要按規則修改差分陣列即可。

image

當對一個區間進行增減某個值的時候,他的差分陣列對應的區間左端點的值會同步變化,而他的右端點的後一個值則會相反地變化

模板和例題

LeetCode 1109. 航班預訂統計

這裡有 n 個航班,它們分別從 1 到 n 進行編號。

有一份航班預訂表 bookings ,表中第 i 條預訂記錄 bookings[i] = [firsti, lasti, seatsi] 意味著在從 firsti 到 lasti (包含 firsti 和 lasti )的 每個航班 上預訂了 seatsi 個座位。

請你返回一個長度為 n 的陣列 answer,其中 answer[i] 是航班 i 上預訂的座位總數。

 
示例 1:

輸入:bookings = [[1,2,10],[2,3,20],[2,5,25]], n = 5
輸出:[10,55,45,25,25]
解釋:
航班編號        1   2   3   4   5
預訂記錄 1 :   10  10
預訂記錄 2 :       20  20
預訂記錄 3 :       25  25  25  25
總座位數:      10  55  45  25  25
因此,answer = [10,55,45,25,25]
示例 2:

輸入:bookings = [[1,2,10],[2,2,15]], n = 2
輸出:[10,25]
解釋:
航班編號        1   2
預訂記錄 1 :   10  10
預訂記錄 2 :       15
總座位數:      10  25
因此,answer = [10,25]
 

提示:

1 <= n <= 2 * 104
1 <= bookings.length <= 2 * 104
bookings[i].length == 3
1 <= firsti <= lasti <= n
1 <= seatsi <= 104

class Solution {
    public int[] corpFlightBookings(int[][] bs, int n) {
        int[] c = new int[n + 1];
        for (int[] bo : bs) {
            int l = bo[0] - 1, r = bo[1] - 1, v = bo[2];
            c[l] += v;
            c[r + 1] -= v;
        }
        int[] ans = new int[n];
        ans[0] = c[0];
        for (int i = 1; i < n; i++) {
            ans[i] = ans[i - 1] + c[i];
        }
        return ans;
    }
}

二維差分

        differ[x1][y1] += c;  //原矩陣(x1,y1)到右下角的值都加c
        differ[x2 + 1][y1] -= c;  //原矩陣(x2+1,y1)到右下角的值都加c
        differ[x1][y2 + 1] -= c;  //原矩陣(x1,y2+1)到右下角的值都加c
        differ[x2 + 1][y2 + 1] += c;  //原矩陣(x2+1,y2+1)到右下角多減去了c,再加回來

image

模板和例題

給定一個二維矩陣 matrix,以下請求:

令子矩形範圍內每個元素的加或減一個常數c,該子矩陣的左上角為 (row1, col1) ,右下角為 (row2, col2) 。

依次輸入二維矩陣, (row1, col1) ,(row2, col2) ,c

並返回處理後的二維矩陣

示例1:
輸入:matrix = [[1,2,3],[4,5,6],[7,8,9]],1,1,2,2,1
輸出:[[1,2,3],[4,6,7],[7,9,10]]

image

public class demo1 {

    static int[][] differ;  //二維差分陣列
    static int m, n;

    public static void main(String[] args) {
        int[][] matrix = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
        int[][] ans = diff(matrix, 1, 1, 2, 2, 1);
        System.out.println(Arrays.deepToString(ans));
    }

    public static void insert(int x1, int y1, int x2, int y2, int c) {  //差分陣列的構造
        differ[x1][y1] += c;
        differ[x2 + 1][y1] -= c;
        differ[x1][y2 + 1] -= c;
        differ[x2 + 1][y2 + 1] += c;
    }

    public static int[][] diff(int[][] matrix, int x1, int y1, int x2, int y2, int c) {
        m = matrix.length;
        n = m == 0 ? 0 : matrix[0].length;
        differ = new int[m + 2][n + 2];

        //所給matrix下標都從0開始,而差分陣列的下標必須從1,開始,所以下標做一定的調整
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                insert(i + 1, j + 1, i + 1, j + 1, matrix[i][j]);
            }
        }
        insert(x1 + 1, y1 + 1, x2 + 1, y2 + 1, c);  //二維差分矩陣

        //求二維字首和
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                differ[i][j] = differ[i - 1][j] + differ[i][j - 1] - differ[i - 1][j - 1] + differ[i][j];
            }
        }
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                matrix[i - 1][j - 1] = differ[i][j];
            }
        }
        return matrix;
    }
}

//輸出:[[1, 2, 3], [4, 6, 7], [7, 9, 10]]