Problem: 1292. 元素和小于等于阈值的正方形的最大边长
文章目录
- [1. 整体思路](#1. 整体思路)
- [2. 完整代码](#2. 完整代码)
- [3. 时空复杂度](#3. 时空复杂度)
-
-
- [时间复杂度: O ( M × N ) O(M \times N) O(M×N)](#时间复杂度: O ( M × N ) O(M \times N) O(M×N))
- [空间复杂度: O ( M × N ) O(M \times N) O(M×N)](#空间复杂度: O ( M × N ) O(M \times N) O(M×N))
-
1. 整体思路
核心问题
在一个矩阵中,找到一个边长最大的正方形,使得该正方形内所有元素的和小于等于给定的 threshold。
算法逻辑
-
预处理:二维前缀和 (2D Prefix Sum)
- 为了在 O ( 1 ) O(1) O(1) 时间内计算出任意子矩形(正方形)的元素和,代码首先构建了一个辅助数组
sum。 sum[i][j]表示原矩阵中从(0, 0)到(i-1, j-1)矩形区域的元素总和。- 利用容斥原理公式:
当前格前缀和 = 上 + 左 - 左上 + 当前元素值。
- 为了在 O ( 1 ) O(1) O(1) 时间内计算出任意子矩形(正方形)的元素和,代码首先构建了一个辅助数组
-
主逻辑:枚举位置 + 贪心扩展 (Smart Iteration)
- 这也是这段代码最巧妙的地方。
- 我们使用两个循环遍历矩阵的每个位置
(i, j),将其视为正方形的左上角。 - 维护一个变量
ans,表示当前已经找到的最大边长。 - 在遍历每个位置时,我们不需要 从边长 1 开始重新检查,也不需要二分查找边长。
- 我们只需要检查:从当前位置
(i, j)开始,能否形成一个边长为ans + 1的正方形,且其元素和 ≤ \le ≤threshold?- 如果能 :说明我们找到了一个更大的正方形,于是执行
ans++。并在当前位置继续尝试ans + 2(由while循环实现,尽管实际上每次位置移动通常最多增加 1,但在某些特殊情况下可能连续增加)。 - 如果不能 :说明以当前
(i, j)为起点的正方形无法打破当前的记录ans,直接继续遍历下一个位置。
- 如果能 :说明我们找到了一个更大的正方形,于是执行
- 这种策略保证了
ans是单调递增的。我们只关心是否能找到比当前更大的正方形,忽略比当前小或相等的。
2. 完整代码
java
class Solution {
public int maxSideLength(int[][] mat, int threshold) {
int m = mat.length;
int n = mat[0].length;
// 1. 构建二维前缀和数组
// 大小设为 [m+1][n+1] 是为了方便处理边界(第0行和第0列全为0)
int[][] sum = new int[m + 1][n + 1];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 前缀和递推公式:上 + 左 - 左上 + 当前元素
// 注意:sum 数组的索引比 mat 数组大 1
sum[i + 1][j + 1] = sum[i + 1][j] + sum[i][j + 1] - sum[i][j] + mat[i][j];
}
}
// ans 记录全局找到的最大边长
int ans = 0;
// 2. 遍历矩阵的每个格子,作为正方形的左上角
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 核心贪心逻辑:
// 检查以 (i, j) 为左上角,边长为 ans + 1 的正方形是否合法。
// 这里的 i + ans 其实对应的是边长 ans + 1 的右下角坐标偏移量。
// 举例:若 ans=0,检查 i 到 i+0 (长度1);若 ans=1,检查 i 到 i+1 (长度2)。
// 边界检查 (i + ans < m && j + ans < n) 确保正方形不越界
// 阈值检查 (getRectSum(...) <= threshold) 确保元素和满足条件
while (i + ans < m && j + ans < n &&
getRectSum(sum, i, j, i + ans, j + ans) <= threshold) {
// 如果满足条件,说明找到了一个更大的正方形 (边长 ans+1)
// 更新 ans,并尝试继续扩大(while 循环会继续检查 ans+2)
ans++;
}
}
}
return ans;
}
// 辅助方法:利用前缀和在 O(1) 时间内计算子矩形的元素和
// (r1, c1) 是左上角, (r2, c2) 是右下角 (包含边界)
private int getRectSum(int[][] sum, int r1, int c1, int r2, int c2) {
// sum 数组中索引偏移了 1,所以右下角取 r2+1, c2+1
// 左上角要减去的部分对应的索引是 r1, c1 (也就是原图索引 r1, c1 对应的 sum 数组位置)
return sum[r2 + 1][c2 + 1] - sum[r2 + 1][c1] - sum[r1][c2 + 1] + sum[r1][c1];
}
}
3. 时空复杂度
假设矩阵的行数为 M M M,列数为 N N N。
时间复杂度: O ( M × N ) O(M \times N) O(M×N)
- 前缀和构建 :遍历整个矩阵一次,耗时 O ( M × N ) O(M \times N) O(M×N)。
- 主循环与贪心检查 :
- 虽然有双重
for循环和一个内部的while循环,但我们可以这样分析: - 双重
for循环会访问每个格子(i, j)一次。 - 在每次访问时,
getRectSum的计算是 O ( 1 ) O(1) O(1) 的。 - 关键在于
ans变量。ans在整个程序的生命周期中只增不减 。ans的最大值也就限制在 min ( M , N ) \min(M, N) min(M,N)。 - 也就是说,
while循环中的ans++操作总共最多执行 min ( M , N ) \min(M, N) min(M,N) 次。 - 大部分情况下,
while循环的条件判断只会执行一次(失败并退出)或者少数几次(成功并增加ans)。 - 因此,均摊下来,总操作次数与格子数量成正比。
- 虽然有双重
- 结论 : O ( M × N ) O(M \times N) O(M×N)。这是此问题的最优解法。
空间复杂度: O ( M × N ) O(M \times N) O(M×N)
- 计算依据 :
- 我们需要一个大小为 ( M + 1 ) × ( N + 1 ) (M+1) \times (N+1) (M+1)×(N+1) 的二维数组
sum来存储前缀和。
- 我们需要一个大小为 ( M + 1 ) × ( N + 1 ) (M+1) \times (N+1) (M+1)×(N+1) 的二维数组
- 结论 : O ( M × N ) O(M \times N) O(M×N)。