题目
给你一个大小为 m x n 的矩阵 mat 和一个整数阈值 threshold。
请你返回元素总和小于或等于阈值的正方形区域的最大边长;如果没有这样的正方形区域,则返回 0 。
示例 1:

输入:mat = [[1,1,3,2,4,3,2],[1,1,3,2,4,3,2],[1,1,3,2,4,3,2]], threshold = 4 输出:2 解释:总和小于或等于 4 的正方形的最大边长为 2,如图所示。
示例 2:
输入:mat = [[2,2,2,2,2],[2,2,2,2,2],[2,2,2,2,2],[2,2,2,2,2],[2,2,2,2,2]], threshold = 1 输出:0
提示:
m == mat.lengthn == mat[i].length1 <= m, n <= 3000 <= mat[i][j] <= 10^40 <= threshold <= 10^5
解题思路
目标:在一个 $M \times N$ 的矩阵中,找到一个边长为 $k$ 的正方形,使得其元素和 $\le$ threshold,求最大的 $k$。
如果使用最直觉的暴力解法:
- 枚举所有可能的边长 $k$ ($1 \to \min(M,N)$)。
- 枚举所有可能的左上角坐标 $(i, j)$。
- 计算这个 $k \times k$ 正方形里的元素和。
瓶颈在哪里?
第 3 步是最大的瓶颈。每次计算一个区域和,都要遍历 $k^2$ 个元素。
总时间复杂度会达到 $O(M \cdot N \cdot \min(M,N)^3)$。
对于 $300 \times 300$ 的数据,这绝对会超时。
结论:我们需要一种能在 $O(1)$ 时间内算出任意子矩阵和的方法。
为了解决求和慢的问题,我们引入二维前缀和 (2D Prefix Sum)。这是处理矩阵区域和问题的标准“起手式”。
1. 定义
P[i][j] 表示从矩阵左上角 (0, 0) 到位置 (i-1, j-1) (即前 $i$ 行、前 $j$ 列)的矩形区域内所有元素的和。
(注意:为了处理边界方便,P 数组通常比原矩阵多一行一列,下标从 1 开始)
2. 构建公式 (递推)
如何快速填满 P 数组?利用容斥原理:
$$P[i][j] = \underbrace{P[i-1][j]}{\text{上部分}} + \underbrace{P[i][j-1]}{\text{左部分}} - \underbrace{P[i-1][j-1]}{\text{重复减去的左上角}} + \underbrace{mat[i-1][j-1]}{\text{当前元素}}$$
3. 查询公式 ($O(1)$ 求和)
有了 P 数组,计算任意正方形(右下角为 $r, c$,边长为 $k$)的和:
$$Sum = P[r][c] - P[r-k][c] - P[r][c-k] + P[r-k][c-k]$$
这一步完成后,求和操作从 $O(k^2)$ 降维到了 $O(1)$。
现在我们能快速求和了,剩下的问题是怎么找最大的 $k$。这里有两条技术路线,也是面试中展示思维深度的关键。
路线 A:二分查找 (常规解法)
思路:
正方形边长具有单调性。
- 如果边长 $k$ 满足条件(和 $\le$ 阈值),那么 $k-1$ 一定也满足(因为元素非负)。
- 如果边长 $k$ 不满足,那么 $k+1$ 肯定也不满足。
算法:
- 二分枚举边长 $k$ (范围 $0 \to \min(M,N)$)。
check(k)函数:遍历矩阵所有位置,看是否存在一个边长为 $k$ 的正方形满足条件。- 时间复杂度:$O(M \cdot N \cdot \log(\min(M,N)))$。
- 这是完全可以接受的解法。
路线 B:贪心/智能枚举 (最优解法 $O(MN)$)
核心逻辑:
我们不需要对每个位置都重新测试“它能容纳的最大边长是多少”。我们只关心全局最大值是否能被刷新。
类比“跳高比赛”:
- 我们维护一个全局最高纪录
ans(假设当前是 3 米)。 - 当遍历到下一个位置(下一个运动员)时,我们不让他从 1 米开始跳,而是直接问:“你能跳过 4 米 (
ans + 1) 吗?”- 能:太棒了,世界纪录刷新,
ans变成 4。 - 不能:无所谓,我们不关心他到底能跳 2 米还是 3 米,因为那对刷新纪录没帮助。我们带着
ans = 3继续看下一个人。
- 能:太棒了,世界纪录刷新,
代码执行流重现:
- 初始化
ans = 0(或者 1)。 - 遍历矩阵的每一个位置
(r, c)作为正方形的右下角。 - 只检查:以
(r, c)为右下角,边长为ans + 1的正方形。- 检查1 (边界):
r和c够大吗?(比如ans+1是 5,你坐标才(3,3),那肯定放不下)。 - 检查2 (数值):这个区域的和 $\le$
threshold吗?
- 检查1 (边界):
- 如果上述两个都满足:说明我们找到了一个更大的正方形,
ans++。 - 如果不满足:
ans不变,继续下一个位置。
复杂度分析
时间复杂度 (Time Complexity)
总复杂度:$O(M \cdot N)$
我们可以将代码拆解为两个主要部分:
- 部分 A:预处理(构建前缀和数组)
- 我们需要遍历整个矩阵一次来填充
prefix数组。 - 对于矩阵中的每个位置 $(i, j)$,执行的计算是加减法,耗时 $O(1)$。
- 这一步的复杂度是 $O(M \cdot N)$。
- 我们需要遍历整个矩阵一次来填充
- 部分 B:贪心枚举(查找最大边长)
- 这是一个双重循环:外层遍历行 ($1 \dots M$),内层遍历列 ($1 \dots N$)。
- 总共迭代次数是 $M \cdot N$ 次。
- 循环内部的操作:
testLen = ans + 1(加法,$O(1)$)- 边界检查
row >= testLen($O(1)$) getSum函数调用 (4次数组访问 + 3次加减法,$O(1)$)ans++($O(1)$)
- 虽然我们在寻找最大值,但
ans变量在整个过程中只增不减。内部没有额外的循环(不像暴力法那样还要遍历边长)。 - 这一步的复杂度也是 $O(M \cdot N)$。
结论:
$$O(M \cdot N) + O(M \cdot N) = O(M \cdot N)$$
这是该问题的理论下界(Lower Bound)。因为要解决这个问题,你至少需要读取输入矩阵中的每一个元素一次,所以不可能有优于 $O(MN)$ 的算法。
空间复杂度 (Space Complexity)
总复杂度:$O(M \cdot N)$
- 辅助空间:我们需要一个大小为 $(M+1) \times (N+1)$ 的整数数组
prefix来存储前缀和。 - 占用内存:假设 $M, N = 300$,
int为 4 字节。- $300 \times 300 \times 4 \text{ Bytes} \approx 360 \text{ KB}$。
- 这在算法竞赛或工程中都是非常小的开销。
进阶优化(In-place Optimization,仅供参考):
如果不允许使用额外空间(且允许修改原数组 mat),我们可以直接在 mat 上计算前缀和。
$$mat[i][j] \leftarrow mat[i][j] + mat[i-1][j] + \dots$$
这样空间复杂度可以降为 $O(1)$(不计算输入本身的空间)。但在实际工程中,为了保持数据不可变性(Immutability),通常不建议这么做,除非内存极其紧张。
| 算法思路 | 时间复杂度 |
|---|---|
| 暴力法 (对每个点枚举所有边长求和) | $O(M \cdot N \cdot \min(M,N)^3)$ |
| 前缀和 + 暴力枚举边长 | $O(M \cdot N \cdot \min(M,N))$ |
| 前缀和 + 二分查找边长 | $O(M \cdot N \cdot \log(\min(M,N)))$ |
| 前缀和 + 贪心枚举 | $O(M \cdot N)$ |
具体代码
func maxSideLength(mat [][]int, threshold int) int {
m := len(mat)
n := len(mat[0])
prefix := make([][]int, m + 1)
for i := range prefix {
prefix[i] = make([]int, n + 1)
}
minNum := 10000
for i := 1; i < m + 1; i++ {
for j := 1; j < n + 1; j++ {
prefix[i][j] = prefix[i - 1][j] + prefix[i][j - 1] - prefix[i - 1][j - 1] + mat[i - 1][j - 1]
minNum = min(minNum, mat[i - 1][j - 1])
}
}
if minNum > threshold {
return 0
}
getSum := func(i ,j, length int) int {
return prefix[i][j] - prefix[i - length][j] - prefix[i][j - length] + prefix[i - length][j - length]
}
ans := 1
for row := 1; row < m + 1; row++ {
for col := 1; col < n + 1; col++ {
testLen := ans + 1
if row - testLen >= 0 && col - testLen >= 0 {
if getSum(row, col, testLen) <= threshold {
ans++
}
}
}
}
return ans
}