矩阵区域和 图解题解
这道题到底在问什么
- 输入
- mat = [[1,2,3],[4,5,6],[7,8,9]], k = 1
- 输出
- answer[1][1] = 整个矩阵的和 = 45
- 输入
- 同上, answer[0][0]
- 输出
- 左上角窗口被剪成 2 乘 2:1+2+4+5 = 12
- 输入
- 同上, answer[2][2]
- 输出
- 右下角窗口剪成 2 乘 2:5+6+8+9 = 28
最优解:为什么这么做
一句话答案:LeetCode 1314 矩阵区域和:每格要算以它为中心一个方块里的和,用二维前缀和一次建表、每格四项容斥算块和,把区域求和从 O(k²) 压到 O(1),总时间 O(m·n)。
answer[i][j] 到底把哪些格子加起来
给一个 m 行 n 列矩阵 mat 和整数 k,输出同样大小的 answer:每个 answer[i][j] 是以 (i,j) 为中心、边长 2k+1 的方块里所有数之和,探出矩阵的部分剪掉。题面 mat=[[1,2,3],[4,5,6],[7,8,9]]、k 取 1,answer[1][1] 盖满全矩阵、值 45。
逐格累加每个窗口,卡在哪
把每个 answer[i][j] 拆开逐格加,外两重循环走遍 m·n 个中心,里两重循环扫窗口最多 (2k+1)² 个格子,四重、O(m·n·k²)。k 一大,同一格被周围一圈中心反复加,全是白工。
二维前缀和:一次建表,每格四项算块和
先建二维前缀和表 s,让 s[i][j] 存原矩阵左上角到 (i-1,j-1) 这块矩形的总和。有了它,任意矩形区域的和拿四个角一加减就得出——大块减上条、减左条、再加回左上块,即四项容斥。区域求和从 O(k²) 降到 O(1)。
建表怎么递推、查块前先把窗口钳进矩阵
建表 s 开成 (m+1) 乘 (n+1),最上一行最左一列全填 0 当哨兵。每个 s[i][j] = 上面一块 s[i-1][j] 加左边一块 s[i][j-1],两块在左上小块 s[i-1][j-1] 上重叠、减一次,再补当前格的值。有了这圈零,第一行第一列的越界项天然取 0,不必单独判边界。
算 answer[i][j] 前先把窗口钳进矩阵:x1=max(i-k,0)、y1=max(j-k,0) 定左上角,x2=min(m-1,i+k)、y2=min(n-1,j+k) 定右下角。中心在角或边上时 i-k 变负、i+k 越末行,不钳就越界。查表带 +1 偏移:哨兵让下标右移一格,右下角查 s[x2+1][y2+1]、左上角查 s[x1][y1],故下一节右下是 s[3]、左上是 s[0]。
拿题面矩阵把三个 answer 亲手算一遍
先填 s。哨兵那圈是 0,s[1][1]=0+0-0+1=1,s[1][2]=3、s[1][3]=6;第二行 s[2] 是 5、12、21;第三行 s[3] 是 12、27,s[3][3]=21+27-12+9=45,正是九个数总和。
算 answer[1][1]:窗口盖满矩阵,钳完行 0 到 2、列 0 到 2,s[3][3]-s[0][3]-s[3][0]+s[0][0]=45。算 answer[0][0]:窗口行 -1 到 1、列 -1 到 1 钳成行 0 到 1、列 0 到 1,s[2][2]-s[0][2]-s[2][0]+s[0][0]=12,正好是 1+2+4+5。
复杂度,还有两处最坑的偏移
两段扫描都线性:建表 O(m·n)、每格查表四次加减合起来 O(m·n),比逐格累加的 O(m·n·k²) 快得多;前缀表 s 占 (m+1)×(n+1)、空间 O(m·n)。两处偏移最坑:前缀表若不多开那一圈零,第一行第一列就得单独写边界 if;四角的偏移正负号一记混,抠出的矩形整个错位。边界也好验:k 盖满全矩阵时每格是总和 45,k 为 0 时原样返回 mat。
▶ 动画逐步走查(共 24 步)——想跟着动画一帧帧对照就展开
- 3记住两句:建表是「上加左减左上再加当前」;查矩形和是「大块减上条减左条加左上块」。窗口边界先用 max、min 钳进矩阵,再套查询公式。
- 4这是要处理的 3 乘 3 矩阵,行号 i 从上往下 0 到 2,列号 j 从左往右 0 到 2,数值就是 1 到 9 顺着排。k 等于 1,意味着每个中心格要把自己和周围一圈共九个格子加起来,挨着边或角的中心则只能加到落在矩阵里的那几格。先把这张原图记在心里,接下来去搭前缀和表。
- 5前缀和表 s 比原矩阵多一行一列,这里是 4 乘 4。最上面那一行和最左边那一列全部预填 0,它们是哨兵,代表「一个格子都还没框进来」时的和。有了这圈零,后面递推到第一行第一列时就不用单独写边界判断。中间打点的格子是待会儿要逐个填的真实前缀和。
- 6填 s[1][1],它代表原矩阵左上角到第 0 行第 0 列这块的总和。把上面一块 0 和左边一块 0 加起来,这俩在左上角那一小块 0 上重复算了一次,减掉它,再补上当前格自己的 1,得到 1。
- 7填 s[1][2],它代表原矩阵左上角到第 0 行第 1 列这块的总和。把上面一块 0 和左边一块 1 加起来,这俩在左上角那一小块 0 上重复算了一次,减掉它,再补上当前格自己的 2,得到 3。
- 8填 s[1][3],它代表原矩阵左上角到第 0 行第 2 列这块的总和。把上面一块 0 和左边一块 3 加起来,这俩在左上角那一小块 0 上重复算了一次,减掉它,再补上当前格自己的 3,得到 6。
- 9填 s[2][1],它代表原矩阵左上角到第 1 行第 0 列这块的总和。把上面一块 1 和左边一块 0 加起来,这俩在左上角那一小块 0 上重复算了一次,减掉它,再补上当前格自己的 4,得到 5。
- 10填 s[2][2],它代表原矩阵左上角到第 1 行第 1 列这块的总和。把上面一块 3 和左边一块 5 加起来,这俩在左上角那一小块 1 上重复算了一次,减掉它,再补上当前格自己的 5,得到 12。
- 11填 s[2][3],它代表原矩阵左上角到第 1 行第 2 列这块的总和。把上面一块 6 和左边一块 12 加起来,这俩在左上角那一小块 3 上重复算了一次,减掉它,再补上当前格自己的 6,得到 21。
- 12填 s[3][1],它代表原矩阵左上角到第 2 行第 0 列这块的总和。把上面一块 5 和左边一块 0 加起来,这俩在左上角那一小块 0 上重复算了一次,减掉它,再补上当前格自己的 7,得到 12。
- 13填 s[3][2],它代表原矩阵左上角到第 2 行第 1 列这块的总和。把上面一块 12 和左边一块 12 加起来,这俩在左上角那一小块 5 上重复算了一次,减掉它,再补上当前格自己的 8,得到 27。
- 14填 s[3][3],它代表原矩阵左上角到第 2 行第 2 列这块的总和。把上面一块 21 和左边一块 27 加起来,这俩在左上角那一小块 12 上重复算了一次,减掉它,再补上当前格自己的 9,得到 45。
- 15九个真实前缀和全部填好。最右下角 s[3][3] 等于 45,刚好是整个矩阵九个数的总和,可以拿来自检。现在这张表的威力在于:原矩阵里任意一个矩形区域的和,都能用它四个角的值一加减算出来,不用再去逐格累加。下面就用它来回答每个 answer[i][j]。
- 16现在算中心在 (0,0) 的窗口。理论窗口是行 -1 到 1、列 -1 到 1。窗口探出了矩阵,被钳成矩形 行 0..1、列 0..1。点亮的这几格 1 + 2 + 4 + 5 就是要相加的区域,直接加是 12,接下来用前缀表一步算出同样的结果。
- 17钳好的矩形是行 0 到 1、列 0 到 1。套区域查询公式:大块到右下角 s[2][2] 是 12,减掉上面那条 s[0][2] = 0,再减掉左边那条 s[2][0] = 0,左上角那块被多减了一次要补回来 s[0][0] = 0。12 减 0 减 0 加 0 等于 12,和刚才直接相加完全一致。
- 18现在算中心在 (0,1) 的窗口。理论窗口是行 -1 到 1、列 0 到 2。窗口探出了矩阵,被钳成矩形 行 0..1、列 0..2。点亮的这几格 1 + 2 + 3 + 4 + 5 + 6 就是要相加的区域,直接加是 21,接下来用前缀表一步算出同样的结果。
- 19钳好的矩形是行 0 到 1、列 0 到 2。套区域查询公式:大块到右下角 s[2][3] 是 21,减掉上面那条 s[0][3] = 0,再减掉左边那条 s[2][0] = 0,左上角那块被多减了一次要补回来 s[0][0] = 0。21 减 0 减 0 加 0 等于 21,和刚才直接相加完全一致。
- 20现在算中心在 (1,1) 的窗口。理论窗口是行 0 到 2、列 0 到 2。窗口正好落在矩阵内,不需要钳,行 0..2、列 0..2。点亮的这几格 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 就是要相加的区域,直接加是 45,接下来用前缀表一步算出同样的结果。
- 21钳好的矩形是行 0 到 2、列 0 到 2。套区域查询公式:大块到右下角 s[3][3] 是 45,减掉上面那条 s[0][3] = 0,再减掉左边那条 s[3][0] = 0,左上角那块被多减了一次要补回来 s[0][0] = 0。45 减 0 减 0 加 0 等于 45,和刚才直接相加完全一致。
- 22现在算中心在 (1,2) 的窗口。理论窗口是行 0 到 2、列 1 到 3。窗口探出了矩阵,被钳成矩形 行 0..2、列 1..2。点亮的这几格 2 + 3 + 5 + 6 + 8 + 9 就是要相加的区域,直接加是 33,接下来用前缀表一步算出同样的结果。
- 23钳好的矩形是行 0 到 2、列 1 到 2。套区域查询公式:大块到右下角 s[3][3] 是 45,减掉上面那条 s[0][3] = 0,再减掉左边那条 s[3][1] = 12,左上角那块被多减了一次要补回来 s[0][1] = 0。45 减 0 减 12 加 0 等于 33,和刚才直接相加完全一致。
- 24现在算中心在 (2,2) 的窗口。理论窗口是行 1 到 3、列 1 到 3。窗口探出了矩阵,被钳成矩形 行 1..2、列 1..2。点亮的这几格 5 + 6 + 8 + 9 就是要相加的区域,直接加是 28,接下来用前缀表一步算出同样的结果。
- 25钳好的矩形是行 1 到 2、列 1 到 2。套区域查询公式:大块到右下角 s[3][3] 是 45,减掉上面那条 s[1][3] = 6,再减掉左边那条 s[3][1] = 12,左上角那块被多减了一次要补回来 s[1][1] = 1。45 减 6 减 12 加 1 等于 28,和刚才直接相加完全一致。
- 26把九个 answer 都按同样两步算出来,就是这张结果矩阵。正中 answer[1][1] 的窗口覆盖整张矩阵,值是 45;四个角因为窗口被剪成 2 乘 2,值最小;四条边上是 2 乘 3。每一格都只花常数步,这就是二维前缀和的好处。
⚠️ 容易写错的地方
✗ 错:前缀表只开和原矩阵一样大,没多那一圈零
✓ 对:开 (m+1)×(n+1),第 0 行第 0 列留 0 当哨兵
有了哨兵,递推到第一行第一列时 s[i][j+1] 等项天然取到 0,不必为边界单独写 if;查询时 x1、y1 也可能取到 0 这一圈,正好对应「没有上条 / 左条」
✗ 错:窗口边界忘了钳,直接用 i-k 或 i+k 去查
✓ 对:x1=max(i-k,0)、y1=max(j-k,0)、x2=min(m-1,i+k)、y2=min(n-1,j+k)
中心在角或边上时,i-k 会变负、i+k 会越过末行,不钳就会下标越界或把矩阵外的虚无格子算进来,答案就错了
✗ 错:区域查询公式四个角的下标或正负号记混
✓ 对:s[x2+1][y2+1] - s[x1][y2+1] - s[x2+1][y1] + s[x1][y1]
大块用右下角的 x2+1、y2+1;减去的上条、左条用 x1 或 y1;左上块被两条各减了一次,要加一次补回来。下标都带着那个 +1 偏移,别和原矩阵坐标混用
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def matrixBlockSum(self, mat: List[List[int]], k: int) -> List[List[int]]:
m, n = len(mat), len(mat[0])
s = [[0] * (n + 1) for _ in range(m + 1)]
for i, row in enumerate(mat, 1):
for j, x in enumerate(row, 1):
s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + x
ans = [[0] * n for _ in range(m)]
for i in range(m):
for j in range(n):
x1, y1 = max(i - k, 0), max(j - k, 0)
x2, y2 = min(m - 1, i + k), min(n - 1, j + k)
ans[i][j] = (
s[x2 + 1][y2 + 1] - s[x1][y2 + 1] - s[x2 + 1][y1] + s[x1][y1]
)
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k) {
int m = mat.size();
int n = mat[0].size();
vector<vector<int>> s(m + 1, vector<int>(n + 1));
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
s[i + 1][j + 1] = s[i][j + 1] + s[i + 1][j] - s[i][j] + mat[i][j];
}
}
vector<vector<int>> ans(m, vector<int>(n));
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
int x1 = max(i - k, 0);
int y1 = max(j - k, 0);
int x2 = min(m - 1, i + k);
int y2 = min(n - 1, j + k);
ans[i][j] = s[x2 + 1][y2 + 1] - s[x1][y2 + 1] - s[x2 + 1][y1] + s[x1][y1];
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int[][] matrixBlockSum(int[][] mat, int k) {
int m = mat.length;
int n = mat[0].length;
int[][] s = new int[m + 1][n + 1];
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
s[i + 1][j + 1] = s[i][j + 1] + s[i + 1][j] - s[i][j] + mat[i][j];
}
}
int[][] ans = new int[m][n];
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
int x1 = Math.max(i - k, 0);
int y1 = Math.max(j - k, 0);
int x2 = Math.min(m - 1, i + k);
int y2 = Math.min(n - 1, j + k);
ans[i][j] = s[x2 + 1][y2 + 1] - s[x1][y2 + 1] - s[x2 + 1][y1] + s[x1][y1];
}
}
return ans;
}
}复杂度
时间
O(m·n)
建前缀表扫一遍全矩阵是 O(m·n);算答案再扫一遍全矩阵,每个 answer[i][j] 靠前缀表只做四次加减、常数时间。两段都是线性,合起来 O(m·n)。比逐格累加窗口的 O(m·n·k²) 快得多
空间
O(m·n)
按峰值算:前缀和表 s 是 (m+1)×(n+1),和原矩阵同量级,平方级。输出矩阵 answer 也是 m×n 但通常算进结果不计;没有递归,不额外吃栈
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 矩阵区域和 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
二维前缀和的建表递推为什么是「上加左减左上再加当前」?+
s[i][j] 要的是左上角到当前格这块大矩形的和。上面那块 s[i-1][j] 盖住了上面几行,左边那块 s[i][j-1] 盖住了左边几列,这两块在「上面几行且左边几列」那个左上小矩形 s[i-1][j-1] 上重叠了,两块相加就把它算了两遍,所以要减掉一个,最后补上当前格自己的值。这就是容斥:两块相加、减去重叠、补上新增。
钳窗口那步能省吗,直接用 i-k、i+k 去查会怎样?+
不能省。中心落在角或边上时,i-k 会算成负数、i+k 会越过末行末列,直接拿去当下标要么越界报错,要么把矩阵外根本不存在的格子算进来,答案就错。x1=max(i-k,0)、y1=max(j-k,0) 把左上角拦在 0,x2=min(m-1,i+k)、y2=min(n-1,j+k) 把右下角拦在末行末列,窗口才被剪成界内那块真实矩形。
不建前缀表、直接对每个窗口累加行不行,慢多少?+
行,但慢。每个 answer[i][j] 的窗口最多 (2k+1)² 个格子,对全部 m·n 个中心都逐格加,是 O(m·n·k²),k 大时很贵。前缀和把区域求和从 O(k²) 降到 O(1),总复杂度压到 O(m·n)、和 k 无关:一次预处理,之后每次区域查询都是常数时间。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 矩阵区域和 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。