题目描述

✅ LCR 013. 二维区域和检索 - 矩阵不可变

image-20260928234821249

image-20260928234821250

image-20260928234821254

题意分析

构造时给定一个不会再修改的矩阵,之后多次查询指定子矩形内的元素总和。查询由左上角 (row1, col1) 和右下角 (row2, col2) 给出,四条边上的元素都包含在内。

矩阵元素可以为负,查询区域也可能贴着第一行或第一列。数据固定、查询重复,适合先整理所有左上前缀区域的和,让每次查询只做固定次数的加减。

官方题面同时声明 int sumRegion 和最大 200 × 200、单格绝对值不超过 10^5,因此数值范围本身允许区域和达到 4 × 10^9,并没有给出“所有结果保证不超过32位”的承诺。下面用64位保存前缀及中间计算,保留题面返回接口;最终转为 int 只适用于结果在该返回类型范围内的查询。要覆盖这些超出范围的查询,返回类型也必须相应扩大。

解法:二维前缀和

核心思路

[!blue]

定义 s[i][j] 为原矩阵前 i 行、前 j 列的总和,即覆盖 [0, i) × [0, j)。前缀表比原矩阵多一行一列,s[0][*]、s[*][0] 表示空区域,初值全部为零。

构造 s[i][j] 时,上方前缀和左方前缀合起来已经覆盖所需区域的大部分,但左上重叠部分被加了两次,还缺右下角的新元素。因此:

s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + matrix[i - 1][j - 1]。

查询时从包含右下角的大前缀 s[row2 + 1][col2 + 1] 出发,减去查询上方的前缀和左方的前缀。这两个被减区域的左上交集被重复减了一次,所以还要加回 s[row1][col1]。

由此得到四项查询式:s[row2 + 1][col2 + 1] - s[row2 + 1][col1] - s[row1][col2 + 1] + s[row1][col1]。右下角加一是为了把闭区间端点包含进来;左上角不加一,减掉的恰好是它之前的部分。

查询贴边时会访问那一圈全零前缀,仍适用同一公式。构造与查询都只使用加减,负数不影响容斥;64位前缀使大区域和保持真实值,避免中间表发生32位回绕。

解题步骤

  1. 创建 (m + 1) × (n + 1) 的64位前缀表,保留首行首列为零。
  2. 从前缀下标一开始填表,将上方与左方相加,减去重叠区域,再加对应原元素。
  3. 查询时读取右下大前缀、上方前缀、左方前缀及重复减掉的左上前缀。
  4. 按加减公式得到64位区域和,再按保留的题面接口转换返回;返回类型的可表示范围仍需单独满足。

代码实现

class NumMatrix {
    private long[][] s;

    public NumMatrix(int[][] matrix) {
        int m = matrix.length;
        int n = matrix[0].length;

        // 多开一圈:s[0][*] 与 s[*][0] 恒为 0,代表空矩形。
        s = new long[m + 1][n + 1];

        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                // 加上面 + 加左边 - 减重叠 + 补当前元素。
                s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + matrix[i - 1][j - 1];
            }
        }
    }

    public int sumRegion(int row1, int col1, int row2, int col2) {
        // 右下角要含自身故 +1,左上角不加一以排除其之前的部分。
        return (int) (s[row2 + 1][col2 + 1] - s[row2 + 1][col1] - s[row1][col2 + 1] + s[row1][col1]);
    }
}
type NumMatrix struct {
    s [][]int64
}

func Constructor(matrix [][]int) NumMatrix {
    m, n := len(matrix), len(matrix[0])
    s := make([][]int64, m+1)
    for i := 0; i < m+1; i++ {
        s[i] = make([]int64, n+1)
    }
    for i := 1; i <= m; i++ {
        for j := 1; j <= n; j++ {
            s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + int64(matrix[i-1][j-1])
        }
    }
    return NumMatrix{s}
}

func (this *NumMatrix) SumRegion(row1 int, col1 int, row2 int, col2 int) int {
    return int(this.s[row2+1][col2+1] - this.s[row2+1][col1] - this.s[row1][col2+1] + this.s[row1][col1])
}

复杂度分析

设矩阵有 $m$ 行、$n$ 列。

  • 时间复杂度:构造为 $O(mn)$,每次查询为 $O(1)$,与查询矩形的面积无关。
  • 辅助空间复杂度:$O(mn)$,保存二维前缀表,之后查询无需再扫描原矩阵。

关键点总结

[!green]

  • 前缀下标表示行列数量,统一使用左闭右开语义。
  • 建表减掉重复相加的左上区域,查询加回重复减掉的左上区域。
  • 首行首列的零区域统一处理贴边查询。
  • 存储类型与返回类型都必须覆盖各自的数值范围,扩大前缀类型不会扩大返回接口的范围。

易错点总结

[!yellow]

  • 建表访问原元素时要把前缀下标各减一,不能混用两套下标。
  • 查询右下角需要加一,否则会漏掉最后一行或一列。
  • 查询减去上方和左方后,必须加回被重复减掉的左上区域。
  • 前缀和与容斥中间值可能超出32位,存储和计算都应使用宽整数。
  • 最后直接转成窄整数并不会扩大接口的范围,超范围查询仍不能由32位返回值正确表示。
  • 本方案用于矩阵不变的场景,修改原数据后旧前缀表不会自动更新。

相似题目

题目 难度 关联与区别
303. 区域和检索 - 数组不可变 简单 把一维前缀差推广到二维矩形,需要四个角做容斥。
307. 区域和检索 - 数组可修改 中等 原题存在更新操作,静态前缀和不再适合每次重建,需要树状数组等动态结构。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/60641491
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!