Skip to content

二维前缀和,二维差分,离散化技巧


二维前缀和

原理解析


模板题目

https://leetcode.cn/problems/range-sum-query-2d-immutable/

代码实现

java
class NumMatrix {

    public int[][] sum;

    public NumMatrix(int[][] matrix) {
        int n = matrix.length;
        int m = matrix[0].length;
        // 防止越界,在矩阵的左边和上方包一圈 0
        sum = new int[n + 1][m + 1];
        for (int a = 1, c = 0; c < n; a++, c++) {
            for (int b = 1, d = 0; d < m; b++, d++) {
                // 平移处理
                // 原来的 i,j 位置的值拷贝到 i+1,j+1 位置
                sum[a][b] = matrix[c][d];
            }
        }

        // 计算前缀和
        // 包了 0,起始位置为 1,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];
            }
        }
    }

    // 原来是 row 1,col 1, row 2,col 2,这里改成 a,b,c,d
    public int sumRegion(int a, int b, int c, int d) {
        // 平移前的前缀和查询公式
        // sum[c][d] - sum[c][b-1] - sum[a-1][d] + sum[a-1][b-1]
        // 因为实在平移后的矩阵中算前缀和,所以 c++,d++
        // 因为在原始前缀和查询公式中 a,b 都会减一,又因为是平移处理的
        // 所以 a,b 不用平移,天然的表示减一位置
        // 简单理解:因为要平移,所以对原始前缀和查询公式所有位置 +1 处理
        c++;
        d++;
        return sum[c][d] - sum[c][b] - sum[a][d] + sum[a][b];
    }

    //        public int sumRegion(int a, int b, int c, int d) {
    //            a++;
    //            b++;
    //            c++;
    //            d++;
    //            return sum[c][d] - sum[c][b-1] - sum[a-1][d] + sum[a-1][b-1];
    //        }
}

/**
 * Your NumMatrix object will be instantiated and called as such:
 * NumMatrix obj = new NumMatrix(matrix);
 * int param_1 = obj.sumRegion(row1,col1,row2,col2);
 */

边框都是 1 的最大正方形

题目链接

https://leetcode.cn/problems/largest-1-bordered-square/

思路分析

时间复杂度:O(m * n * min(m,n)),m,n 表示所左上角点的可能位置,min(m,n)表示正方形边长的可能性

如果确定每个正方形后再去遍历边长判断是否都为 1,则复杂度还要再原基础上再 * 4 * min(m,n),而本题优化的点就是在判断这里能不能更快

判断周长是否全为 1,可以用外围的面积减去内部的面积,如果面积差值 = 4 * 边长,那就是合法的正方形,计算面积的过程可以使用二位前缀和实现

只关心每个位置的二维前缀和,用于计算面积进而判断是否合法,原数组并不关心,所以可以复用原数组,将其转为二维前缀和数组,复用后无法补 0,注意边界条件的判断,复用了原数组还节省了空间

空间复杂度:O(1)

代码实现

java
class Solution {
    public int largest1BorderedSquare(int[][] grid) {
        int n = grid.length;
        int m = grid[0].length;
        build(n, m, grid);
        // 特判:如果前缀和都为 0,压根就没有 1
        if (sum(grid, 0, 0, n - 1, m - 1) == 0) {
            return 0;
        }
        // 1 * 1 的正方形肯定存在,找更大的正方形
        int ans = 1;
        // 枚举所有可能的左上角顶点
        for (int a = 0; a < n; a++) {
            for (int b = 0; b < m; b++) {
                // 左上角顶点 (a,b),右下角顶点 (c,d),枚举可能的边长
                // +ans 表示剪枝,在原来相对大的正方形的基础上找更大的
                // 不要重复遍历已经遍历过的部分,边长 k 从 ans + 1 开始
                for (int c = a + ans, d = b + ans, k = ans + 1; c < n && d < m; c++, d++, k++) {
                    // 找到了更大的正方形边长,更新答案
                    // 外层大正方形边长为 k,内层正方形的周长:(k - 1) * 2
                    if (sum(grid, a, b, c, d) - sum(grid, a + 1, b + 1, c - 1, d - 1) == (k - 1) << 2) {
                        ans = k;
                    }
                }
            }
        }
        return ans * ans;

    }

    // 将原始数组 g 变成前缀和数组,复用自己
    // 注意这里无法补 0,需要注意边界判断
    // 计算二维前缀和:左 + 上 - 左上 + 自己
    public static void build(int n, int m, int[][] g) {
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                g[i][j] += get(g, i, j - 1) + get(g, i - 1, j) - get(g, i - 1, j - 1);
            }
        }
    }

    // 二维前缀和查询,左上角顶点 (a,b),右下角顶点 (c,d)
    public static int sum(int[][] g, int a, int b, int c, int d) {
        // 加上特判,例如只有 2 * 2 的正方形,就没有内部空间,但是计算后 a,c 错位了
        return a > c ? 0 : (g[c][d] - get(g, c, b - 1) - get(g, a - 1, d) + get(g, a - 1, b - 1));
    }

    // 由于无法补 0,写一个方法处理越界情况
    public static int get(int[][] g, int i, int j) {
        return (i < 0 || j < 0) ? 0 : g[i][j];
    }
}

二维差分

原理分析

⚠️ 注意:会在矩阵外层包一圈 0,可以避免很多边界的讨论


模板题目

https://www.luogu.com.cn/problem/P3397

代码实现

java
import java.io.*;

public class Main {

    // 外围会包一层 0 ,避免了很多边界处理
    // 对于行和列都多出了 2 行(列),所以要加 2
    public static int MAXN = 1002;

    public static int[][] diff = new int[MAXN][MAXN];

    public static int n, q;

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StreamTokenizer in = new StreamTokenizer(br);
        PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
        while (in.nextToken() != StreamTokenizer.TT_EOF) {
            n = (int) in.nval;
            in.nextToken();
            q = (int) in.nval;
            for (int i = 1, a, b, c, d; i <= q; i++) {
                in.nextToken();
                a = (int) in.nval;
                in.nextToken();
                b = (int) in.nval;
                in.nextToken();
                c = (int) in.nval;
                in.nextToken();
                d = (int) in.nval;
                add(a, b, c, d, 1);
            }
            build();
            for (int i = 1; i <= n; i++) {
                out.print(diff[i][1]);
                for (int j = 2; j <= n; j++) {
                    out.print(" " + diff[i][j]);
                }
                out.println();
            }
            clear();
        }
        out.flush();
        out.close();
        br.close();
    }

    // 差分处理
    public static 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;
    }

    // 加工前缀和
    public static void build() {
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                // 左 + 上 - 左上 + 自己
                diff[i][j] += diff[i][j - 1] + diff[i - 1][j] - diff[i - 1][j - 1];
            }
        }
    }

    // 采用静态空间,清理脏数据,不影响下一组测试用例
    public static void clear() {
        for (int i = 1; i <= n + 1; i++) {
            for (int j = 1; j <= n + 1; j++) {
                diff[i][j] = 0;
            }
        }
    }
}

二维前缀和 + 差分

题目链接

https://leetcode.cn/problems/stamping-the-grid/

思路分析

1 表示不能贴邮票的区域,0 表示可以贴邮票的区域

遍历每一个 0,来到一个 0 位置判断能不能贴

二维前缀和:当前 0 位置开始找出邮票大小的区域,如果前缀和 > 0,说明该区域有 0,不能贴邮票

二维差分:贴了邮票,该区域就不能贴邮票了,利用差分将该区域都修改为 1

如果原始数组中 0 位置的元素在差分数组中对应位置的值是 1,说明 0 位置贴贴上了邮票,否则就没有贴上邮票

最后判断原始矩阵和差分矩阵中对应位置的元素是不是 0,如果是 0,说明没有贴上邮票,返回 false

代码实现

java
class Solution {
    // 时间复杂度O(n*m),额外空间复杂度O(n*m)
    // 原来是 int stampHeight, int stampWidth,这里改成 int h, int w
    public boolean possibleToStamp(int[][] grid, int h, int w) {
        int n = grid.length;
        int m = grid[0].length;
        // sum 是前缀和数组,左边和上面包一层 0,避免边界讨论
        int[][] sum = new int[n + 1][m + 1];
        // 将原始数组的内容拷贝到 sum 数组中,再做前缀和处理
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                sum[i + 1][j + 1] = grid[i][j];
            }
        }
        // 加工前缀和,得到前缀和数组的结果
        build(sum);
        // diff 是差分数组,外层包一圈 0,避免边界讨论
        // 当贴邮票的时候,不在原始矩阵里贴,在差分矩阵里贴
        // 原始矩阵就用来判断能不能贴邮票,不进行修改
        // 每贴一张邮票都在差分矩阵里修改
        int[][] diff = new int[n + 2][m + 2];
        for (int a = 1, c = a + h - 1; c <= n; a++, c++) {
            for (int b = 1, d = b + w - 1; d <= m; b++, d++) {
                if (sumRegion(sum, a, b, c, d) == 0) {
                    // 原始矩阵中 (a,b)左上角点
                    // 根据邮票规格,h、w,算出右下角点(c,d)
                    // 如果这个区域的元素都是 0 ,则说明
                    // sumRegion(sum, a, b, c, d) == 0
                    // 那么此时这个区域可以贴邮票
                    add(diff, a, b, c, d);
                }
            }
        }
        // 加工前缀和,得到差分数组的结果
        build(diff);

        // 检查所有的格子,如果原始数组和差分数组中对应位置的元素为 0
        // 说明不能贴邮票,不符合题意,返回 false
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                // diff 数组是包了一层 0 的,注意平移关系
                if (grid[i][j] == 0 && diff[i + 1][j + 1] == 0) {
                    return false;
                }
            }
        }
        return true;
    }

    // 加工前缀和
    public void build(int[][] m) {
        for (int i = 1; i < m.length; i++) {
            for (int j = 1; j < m[0].length; j++) {
                // 左 + 上 - 左上 + 自己
                m[i][j] += m[i - 1][j] + m[i][j - 1] - m[i - 1][j - 1];
            }
        }
    }

    // 前缀和查询
    public int sumRegion(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];
    }

    // 差分处理
    public void add(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;
    }
}

离散化技巧

原理分析

(1)当边长出现小数,负数,无法转换为数组下标的时候,可以等比例缩放边长

对于本题,用左右上下表示力场的边界,r 表示力场的边长,(x,y)为力场的中心点坐标

X 左 = 2 * x - r,X 右 = 2 * x + r,Y 下 = 2 * y - r,Y 上 = 2 * y + r

(2)当数值的范围很大时,然而题目只关心种类的数量,可以忽略原始的真实大小,关注数量,使用离散化数组压缩存储,排序 + 去重实现,重新编号,每个数值对应一个编号

例如 x 为 120,去重排序后找到 120 对应的数组索引下标,编号 = 下标索引 + 1,计算 120 对应的编号,则后续再出现 120 的时候,就认为 120 对应的编号表示 120 这个位置,而不用根据真实的值开辟空间

原因:(1)如果数值很大,数据需要开辟的空间也很大(2)如果数组中有很多重复的元素,就浪费了很多空间,使用同一个编号就可以表示多个相同的值,同时编号远远比元素的真实值小的多,这样就大大的节省了空间

题目链接

https://leetcode.cn/problems/xepqZ5/

思路分析

本题的叠加力场就是铺地毯场景,即对每一个力场做差分处理,只要有力场存在,则该力场范围内的元素全部加一,最后遍历所有格子,找出最大的格子的值,这就是叠加力场的值

本题有两个点需要离散化处理,一是顶点坐标可能出现小数,二是每个力场中心点坐标和边长会很大,根据题目数据,可以达到 109,显然不可能开辟这么大的内存空间,不关注力场的实际大小,只关注力场的个数,维持相对位置关系即可,相当于等比例缩放

代码实现

java
class Solution {
    // 原来这里是 forceField,这里改成 fields
    // 时间复杂度O(n^2),额外空间复杂度O(n^2),n是力场的个数
    public int fieldOfGreatestBlessing(int[][] fields) {
        int n = fields.length;
        // n :矩阵的个数,一个力场带来两个边界
        long[] xs = new long[n << 1];
        long[] ys = new long[n << 1];
        // 离散化处理 1,等比例缩放,去除小数的影响
        for (int i = 0, k = 0, p = 0; i < n; i++) {
            // x,y 中心坐标,r 正方形的边长
            long x = fields[i][0];
            long y = fields[i][1];
            long r = fields[i][2];
            // 左边界
            xs[k++] = (x << 1) - r;
            // 右边界
            xs[k++] = (x << 1) + r;
            // 下边界
            ys[p++] = (y << 1) - r;
            // 上边界
            ys[p++] = (y << 1) + r;
        }
        // 离散化处理 2,排序 + 去重,压缩存储
        // xs数组中,排序了且相同值只留一份,返回有效长度,ys 同理
        int sizex = sort(xs);
        int sizey = sort(ys);

        // 差分数组,外层包了一圈 0,避免边界讨论
        // n 个力场,sizex:2 * n,sizey:2 * n
        int[][] diff = new int[sizex + 2][sizey + 2];
        for (int i = 0, a, b, c, d; i < n; i++) {
            long x = fields[i][0];
            long y = fields[i][1];
            long r = fields[i][2];
            // 这里不再用真实值,而是用其对应的编号,达到节省空间的目的
            a = rank(xs, (x << 1) - r, sizex);
            b = rank(ys, (y << 1) - r, sizey);
            c = rank(xs, (x << 1) + r, sizex);
            d = rank(ys, (y << 1) + r, sizey);
            add(diff, a, b, c, d);
        }

        int ans = 0;
        for (int i = 1; i < diff.length; i++) {
            for (int j = 1; j < diff[0].length; j++) {
                // 加工前缀和:左 + 上 - 左上 + 自己
                // 加工前缀和的同时同步更新最大力场强度
                diff[i][j] += diff[i - 1][j] + diff[i][j - 1] - diff[i - 1][j - 1];
                ans = Math.max(ans, diff[i][j]);
            }
        }
        return ans;
    }

    // nums 有序数组,有效长度为 size,且 0 ~ size-1 范围上无重复值
    // 已知 v 一定在 nums[0 ~ size -1] 上,返回 v 所对应的编号
    // 使用二分查找实现,优化时间复杂度
    public int rank(long[] nums, long v, int size) {
        int l = 0;
        int r = size - 1;
        int ans = 0;
        while (l <= r) {
            int mid = (l + r) / 2;
            if (nums[mid] >= v) {
                ans = mid;
                r = mid - 1;
            } else {
                l = mid + 1;
            }
        }
        // 注意:返回的是编号,ans 是索引下标
        return ans + 1;
    }

    // 排序 + 去重,返回有效元素的个数,size 控制
    // 返回数组中有效元素的个数
    public int sort(long[] nums) {
        Arrays.sort(nums);
        int size = 1;
        for (int i = 1; i < nums.length; i++) {
            // 不相等就填过来,相同就跳过
            if (nums[i] != nums[size - 1]) {
                nums[size++] = nums[i];
            }
        }
        return size;
    }

    // 二维差分
    public void add(int[][] diff, int a, int b, int c, int d) {
        // 左上方
        diff[a][b] += 1;
        // 右上方
        diff[c + 1][b] -= 1;
        // 左下方
        diff[a][d + 1] -= 1;
        // 右下方
        diff[c + 1][d + 1] += 1;
    }
}