二維字首和與差分、離散化技巧

n1ce2cv發表於2024-10-09

二維字首和

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

二位字首和目的是預處理出一個結構,以後每次查詢二維陣列任何範圍上的累加和都是 O(1) 的操作

  • 根據原始狀況,生成二維字首和陣列sum,

    sum[i][j]: 代表左上角 (0,0) 到右下角 (i,j) 這個範圍的累加和

    sum[i][j] += sum[i][j - 1] + sum[i - 1][j] - sum[i - 1][j - 1];

  • 查詢左上角 (a,b) 到右下角 (c,d) 這個範圍的累加和

    sum[c][d] - sum[c][b-1] - sum[a-1][d] + sum[a-1][b-1];

  • 實際過程中往往補第 0 行、第 0 列來減少很多條件判斷。

#include <vector>

using namespace std;

class NumMatrix {
public:
    vector<vector<int>> sum;

    NumMatrix(vector<vector<int>> &matrix) {
        int n = matrix.size();
        int m = matrix[0].size();
        // 矩陣擴大,減少邊界討論
        sum.resize(n + 1, vector<int>(m + 1));

        // 原始矩陣複製到擴大後的矩陣
        for (int a = 1, c = 0; c < n; a++, c++)
            for (int b = 1, d = 0; d < m; b++, d++)
                sum[a][b] = matrix[c][d];

        // 計算二維字首和
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= m; j++)
                sum[i][j] += sum[i][j - 1] + sum[i - 1][j] - sum[i - 1][j - 1];
    }

    int sumRegion(int row1, int col1, int row2, int col2) {
        row2++;
        col2++;
        return sum[row2][col2] - sum[row2][col1] - sum[row1][col2] + sum[row1][col1];
    }
};

1139. 最大的以 1 為邊界的正方形

#include <vector>

using namespace std;

class Solution {
public:
    // 越界就返回 0
    int get(vector<vector<int>> &grid, int i, int j) {
        return (i < 0 || j < 0) ? 0 : grid[i][j];
    }

    // 把原始矩陣變成二位字首和矩陣
    void build(int n, int m, vector<vector<int>> &grid) {
        for (int i = 0; i < n; i++)
            for (int j = 0; j < m; j++)
                grid[i][j] += get(grid, i, j - 1) + get(grid, i - 1, j) - get(grid, i - 1, j - 1);
    }

    // 返回子矩陣的和
    int sum(vector<vector<int>> &grid, int a, int b, int c, int d) {
        return a > c ? 0 : (grid[c][d] - get(grid, c, b - 1) - get(grid, a - 1, d) + get(grid, a - 1, b - 1));
    }

    // 時間複雜度 O(n * m * min(n,m)),額外空間複雜度 O(1)
    int largest1BorderedSquare(vector<vector<int>> &grid) {
        int n = grid.size();
        int m = grid[0].size();
        build(n, m, grid);
        // 矩陣裡面全是 0
        if (sum(grid, 0, 0, n - 1, m - 1) == 0) return 0;
        // 找到的最大合法正方形的邊長
        int len = 1;
        // (a,b) 所有左上角點,(c,d) 更大邊長的右下角點,k 是當前嘗試的邊長
        for (int a = 0; a < n; a++)
            for (int b = 0; b < m; b++)
                // 從 len + 1 找是為了剪枝,只需要找更長的邊長
                for (int c = a + len, d = b + len, k = len + 1; c < n && d < m; c++, d++, k++)
                    // 如果面積差為周長,說明有一圈 1
                    if (sum(grid, a, b, c, d) - sum(grid, a + 1, b + 1, c - 1, d - 1) == (k - 1) << 2)
                        len = k;
        return len * len;
    }
};

二維差分

在二維陣列中,如果經歷如下的過程

  • 批次的做如下的操作,每個操作都有獨立的 a、b、c、d、v

void add(a, b, c, d, v) : 左上角 (a,b) 到右下角 (c,d) 範圍上,每個數字 +v

// 只對四個點操作
void add(int a, int b, int c, int d, int v) {
	diff[a][b] += v;
	diff[c + 1][b] -= v;
	diff[a][d + 1] -= v;
	diff[c + 1][d + 1] += v;
}
// 構建二維字首和
void build() {
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= n; j++)
			diff[i][j] += diff[i - 1][j] + diff[i][j - 1] - diff[i - 1][j - 1];
}
  • 給矩陣加一圈 0 可以避免邊界討論

【模板】二維差分

#include <iostream>

using namespace std;

const int MAX_N = 1005;
const int MAX_M = 1005;

// 二維差分陣列
long long diff[MAX_N][MAX_M];
int n, m, q;

// 二維差分,對四個點操作
void add(int a, int b, int c, int d, int k) {
    diff[a][b] += k;
    diff[c + 1][b] -= k;
    diff[a][d + 1] -= k;
    diff[c + 1][d + 1] += k;
}

// 計算二維字首和
void build() {
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            diff[i][j] += diff[i - 1][j] + diff[i][j - 1] - diff[i - 1][j - 1];
}

void clear() {
    for (int i = 1; i <= n + 1; i++)
        for (int j = 1; j <= m + 1; j++)
            diff[i][j] = 0;
}

int main() {
    while (cin >> n >> m >> q) {
        // 實際矩陣外圍有一圈 0,避免邊界討論
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                int value;
                cin >> value;
                add(i, j, i, j, value);
            }
        }
        // 二維差分
        for (int i = 1, a, b, c, d, k; i <= q; i++) {
            cin >> a >> b >> c >> d >> k;
            add(a, b, c, d, k);
        }
        build();
        // 列印結果
        for (int i = 1; i <= n; i++) {
            cout << diff[i][1];
            for (int j = 2; j <= m; j++)
                cout << " " << diff[i][j];
            cout << endl;
        }
        clear();
    }

    return 0;
}

P3397 地毯

#include <iostream>

using namespace std;

const int MAXN = 1002;
int diff[MAXN][MAXN];
int n, q;

void add(int a, int b, int c, int d, int k) {
    diff[a][b] += k;
    diff[c + 1][b] -= k;
    diff[a][d + 1] -= k;
    diff[c + 1][d + 1] += k;
}

void build() {
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            diff[i][j] += diff[i - 1][j] + diff[i][j - 1] - diff[i - 1][j - 1];
}

void clear() {
    for (int i = 1; i <= n + 1; i++)
        for (int j = 1; j <= n + 1; j++)
            diff[i][j] = 0;
}

int main() {
    while (cin >> n >> q) {
        for (int i = 1, a, b, c, d; i <= q; i++) {
            cin >> a >> b >> c >> d;
            add(a, b, c, d, 1);
        }
        build();
        for (int i = 1; i <= n; i++) {
            cout << diff[i][1];
            for (int j = 2; j <= n; j++) {
                cout << " " << diff[i][j];
            }
            cout << endl;
        }
        clear();
    }
    return 0;
}

2132. 用郵票貼滿網格圖

#include <iostream>
#include <vector>

using namespace std;

class Solution {
public:
    void add(vector<vector<int>> &diff, int a, int b, int c, int d) {
        diff[a][b] += 1;
        diff[c + 1][d + 1] += 1;
        diff[c + 1][b] -= 1;
        diff[a][d + 1] -= 1;
    }

    void build(vector<vector<int>> &m) {
        for (int i = 1; i < m.size(); i++)
            for (int j = 1; j < m[0].size(); j++)
                m[i][j] += m[i - 1][j] + m[i][j - 1] - m[i - 1][j - 1];
    }

    int sumRegion(vector<vector<int>> &sum, int a, int b, int c, int d) {
        return sum[c][d] - sum[c][b - 1] - sum[a - 1][d] + sum[a - 1][b - 1];
    }

    // 時間複雜度 O(n*m),額外空間複雜度 O(n*m)
    bool possibleToStamp(vector<vector<int>> &grid, int stampHeight, int stampWidth) {
        int n = grid.size();
        int m = grid[0].size();
        // 字首和陣列
        vector<vector<int>> prefixSum(n + 1, vector<int>(m + 1));
        for (int i = 0; i < n; i++)
            for (int j = 0; j < m; j++)
                prefixSum[i + 1][j + 1] = grid[i][j];
        build(prefixSum);

        // 差分矩陣
        // 當貼郵票的時候,不再原始矩陣裡貼,在差分矩陣裡貼
        // 原始矩陣用來判斷能不能貼郵票,不進行修改
        // 每貼一張郵票都在差分矩陣裡修改
        vector<vector<int>> diff(n + 2, vector<int>(m + 2));
        // 原始矩陣中 (a,b) 左上角點,根據 stampHeight、stampWidth,算出右下角點(c,d)
        for (int a = 1, c = a + stampHeight - 1; c <= n; a++, c++)
            for (int b = 1, d = b + stampWidth - 1; d <= m; b++, d++)
                // 這個區域徹底都是 0 時,可以貼郵票
                if (sumRegion(prefixSum, a, b, c, d) == 0)
                    add(diff, a, b, c, d);
        build(diff);
        // 檢查所有的格子
        for (int i = 0; i < n; i++)
            for (int j = 0; j < m; j++)
                // 原始矩陣裡:grid[i][j] == 0,說明是個洞
                // 差分矩陣裡:diff[i + 1][j + 1] == 0,說明洞上並沒有郵票
                // 此時返回 false
                if (grid[i][j] == 0 && diff[i + 1][j + 1] == 0)
                    return false;
        return true;
    }
};

離散化技巧

LCP 74. 最強祝福力場

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

class Solution {
public:
    // 排序並去重
    int mySort(vector<long> &nums) {
        sort(nums.begin(), nums.end());
        int size = 1;
        for (int i = 1; i < nums.size(); i++)
            if (nums[i] != nums[size - 1])
                nums[size++] = nums[i];
        return size;
    }

    // 根據數值二分找下標
    int rank(vector<long> &nums, long v, int size) {
        int l = 0;
        int r = size - 1;
        int m, res = 0;
        while (l <= r) {
            m = (l + r) / 2;
            if (nums[m] >= v) {
                res = m;
                r = m - 1;
            } else {
                l = m + 1;
            }
        }
        return res + 1;
    }

    // 二維差分
    void add(vector<vector<int>> &diff, int a, int b, int c, int d) {
        diff[a][b] += 1;
        diff[c + 1][d + 1] += 1;
        diff[c + 1][b] -= 1;
        diff[a][d + 1] -= 1;
    }

    // 時間複雜度 O(n^2),額外空間複雜度 O(n^2),n 是力場的個數
    int fieldOfGreatestBlessing(vector<vector<int>> &forceField) {
        int n = forceField.size();
        // n 為矩形的個數,2*n 個座標
        vector<long> xs(n << 1);
        vector<long> ys(n << 1);
        for (int i = 0, k = 0, p = 0; i < n; i++) {
            long x = forceField[i][0];
            long y = forceField[i][1];
            long r = forceField[i][2];
            xs[k++] = (x << 1) - r;
            xs[k++] = (x << 1) + r;
            ys[p++] = (y << 1) - r;
            ys[p++] = (y << 1) + r;
        }
        int size_x = mySort(xs);
        int size_y = mySort(ys);

        // n 個力場,size_x : 2 * n, size_y : 2 * n
        vector<vector<int>> diff(size_x + 2, vector<int>(size_y + 2));
        for (int i = 0, a, b, c, d; i < n; i++) {
            long x = forceField[i][0];
            long y = forceField[i][1];
            long r = forceField[i][2];
            a = rank(xs, (x << 1) - r, size_x);
            b = rank(ys, (y << 1) - r, size_y);
            c = rank(xs, (x << 1) + r, size_x);
            d = rank(ys, (y << 1) + r, size_y);
            add(diff, a, b, c, d);
        }
        int res = 0;
        // O(n^2)
        for (int i = 1; i < diff.size(); i++) {
            for (int j = 1; j < diff[0].size(); j++) {
                diff[i][j] += diff[i - 1][j] + diff[i][j - 1] - diff[i - 1][j - 1];
                res = max(res, diff[i][j]);
            }
        }
        return res;
    }
};

相關文章