元素和小于等于阈值的正方形的最大边长 图解题解
这道题到底在问什么
- 输入
- mat = 全是 2 的 5×5,threshold = 1
- 输出
- 0(随便哪个单格都是 2,已经 > 1)
- 输入
- mat = [[1,1,3,2,4,3,2] 重复三行],threshold = 4
- 输出
- 2
- 输入
- 本节演示 3×4 矩阵,threshold = 8
- 输出
- 2
最优解:为什么这么做
一句话答案:LeetCode 1292 元素和≤阈值的最大正方形用二分答案:边长越大越难满足这条单调性,配二维前缀和 O(1) 查子矩阵和,二分最大可行边长。时间 O(m·n·log(min(m,n)))、空间 O(m·n)。
框一个正方形让和不超阈值,返回的是边长还是面积
给一个 m×n 数字矩阵 mat 和阈值 threshold,要框一个正方形子区域(长宽相等),让框住的数之和不超过 threshold,取合格正方形里边长最大的,返回边长 k、不是面积 k×k。题面每行 1 1 3 2 4 3 2 的 3×7 矩阵、阈值 4,答案 2。
每验一个正方形都把格子重加一遍,重得离谱
正方形候选很多:每个左上角配一种边长就是一个,光左上角就 O(m·n) 个。若每验一个都把里面 k×k 个数逐个累加,单次求和 O(k²),再套枚举左上角和边长,判定退化到 O(m·n·k²) 扫不动。慢在「求一块矩形的和」被一遍遍重算。
边长越大越难满足,这条单调让答案能二分
边长 k 能框出合格正方形,更小的 k-1 和更小、更易合格——边长越大越难满足。于是边长切成两段:小的可行、大的不可行,答案是可行段末位。单调就能二分:不直接算,猜一个边长用判定函数验证再往可行侧收,这叫二分答案。二分的是边长这个答案、不是矩阵下标。判定反复求和,先把矩形求和做快。
二维前缀和的四角公式,判定函数怎么写
二维前缀和先算好一张表 s:s[i][j] 存「左上角到某格」这块矩形的和。s 比原矩阵大一圈,第 0 行、第 0 列填 0(哨兵,省边界特判),s[i][j] 对应原矩阵 (i-1,j-1)。建表:s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + mat[i-1][j-1]。查左上角 (i,j)、边长 k 的和用四角(O(1)):s[i+k][j+k] - s[i][j+k] - s[i+k][j] + s[i][j]。判定 check(k) 扫每个放得下的左上角,有一个和 ≤ threshold 就可行,否则不可行。
题面 3×7 矩阵、阈值 4,逐轮试边长
矩阵 3 行 7 列、每行 1 1 3 2 4 3 2、阈值 4。前缀表建完,起点 l=0、r=min(3,7)=3。第一轮 mid=(0+3+1)>>1=2,check(2) 首格 (0,0):s[2][2](左上块和)-s[0][2]-s[2][0]+s[0][0]=4-0-0+0=4≤4,命中,边长 2 可行,l=2。第二轮 mid=(2+3+1)>>1=3,check(3) 首格 (0,0):s[3][3]-0-0+0=15>4,到末位 (0,4) 仍 >4,全失败,r=2;l=r=2,返回 2。
静默出错常藏两处:前缀表下标差一位、中点漏加 1
一个坑在下标:s 大一圈,s[i][j] 对应原矩阵 (i-1,j-1),四角的 s[i+k][j+k] 全靠哨兵不越界,盯错一位整片算歪。另一个在中点:可行时 l=mid,中点须 (l+r+1)>>1 向上取整;写成 (l+r)>>1,区间剩两格时 mid 取到 l 又赋回 l,缩不动就死循环。题目要边长、别返回 k×k 面积。
复杂度:建表 O(m·n),二分 log(min(m,n)) 轮、每轮扫 O(m·n) 个左上角各 O(1) 求和,合起来 O(m·n·log(min(m,n))),空间 O(m·n)。
▶ 动画逐步走查(共 25 步)——想跟着动画一帧帧对照就展开
- 3记住两招:二维前缀和把矩形求和压到 O(1);边长可行性单调,于是二分边长。可行往大找,不可行往小缩。
- 4这是一张 3 行 4 列的矩阵,行号 0 到 2、列号 0 到 3。我们要在它里面框一个正方形,使框住的数字之和不超过 8,并且边长尽量大。第一步不是急着去框,而是先把下面这张前缀和表建出来,它能让后面任何一次正方形求和都只花常数时间。
- 5前缀和表 s 故意比原矩阵大一圈,行列各多出一个 0 号。多出来的第 0 行、第 0 列全部填 0,叫哨兵。有了这圈 0,内部每一格都能统一用「上邻加左邻减左上邻再加本格」这一条公式来算,不用为最上行、最左列单独写边界判断。下面从 i 等于 1、j 等于 1 开始,一行一行把内部填满。
- 6算 s[1][1]:它代表从矩阵左上角一直到原矩阵第 0 行第 0 列(值是 4)这块矩形的总和。正上方那块是 0,正左方那块是 0,这两块把左上角那一小块 0 都算了一遍,重复了,要减掉一次,最后再补上本格自己的 4,得到 4。
- 7算 s[1][2]:它代表从矩阵左上角一直到原矩阵第 0 行第 1 列(值是 3)这块矩形的总和。正上方那块是 0,正左方那块是 4,这两块把左上角那一小块 0 都算了一遍,重复了,要减掉一次,最后再补上本格自己的 3,得到 7。
- 8算 s[1][3]:它代表从矩阵左上角一直到原矩阵第 0 行第 2 列(值是 1)这块矩形的总和。正上方那块是 0,正左方那块是 7,这两块把左上角那一小块 0 都算了一遍,重复了,要减掉一次,最后再补上本格自己的 1,得到 8。
- 9算 s[1][4]:它代表从矩阵左上角一直到原矩阵第 0 行第 3 列(值是 1)这块矩形的总和。正上方那块是 0,正左方那块是 8,这两块把左上角那一小块 0 都算了一遍,重复了,要减掉一次,最后再补上本格自己的 1,得到 9。
- 10算 s[2][1]:它代表从矩阵左上角一直到原矩阵第 1 行第 0 列(值是 2)这块矩形的总和。正上方那块是 4,正左方那块是 0,这两块把左上角那一小块 0 都算了一遍,重复了,要减掉一次,最后再补上本格自己的 2,得到 6。
- 11算 s[2][2]:它代表从矩阵左上角一直到原矩阵第 1 行第 1 列(值是 4)这块矩形的总和。正上方那块是 7,正左方那块是 6,这两块把左上角那一小块 4 都算了一遍,重复了,要减掉一次,最后再补上本格自己的 4,得到 13。
- 12算 s[2][3]:它代表从矩阵左上角一直到原矩阵第 1 行第 2 列(值是 1)这块矩形的总和。正上方那块是 8,正左方那块是 13,这两块把左上角那一小块 7 都算了一遍,重复了,要减掉一次,最后再补上本格自己的 1,得到 15。
- 13算 s[2][4]:它代表从矩阵左上角一直到原矩阵第 1 行第 3 列(值是 1)这块矩形的总和。正上方那块是 9,正左方那块是 15,这两块把左上角那一小块 8 都算了一遍,重复了,要减掉一次,最后再补上本格自己的 1,得到 17。
- 14算 s[3][1]:它代表从矩阵左上角一直到原矩阵第 2 行第 0 列(值是 3)这块矩形的总和。正上方那块是 6,正左方那块是 0,这两块把左上角那一小块 0 都算了一遍,重复了,要减掉一次,最后再补上本格自己的 3,得到 9。
- 15算 s[3][2]:它代表从矩阵左上角一直到原矩阵第 2 行第 1 列(值是 1)这块矩形的总和。正上方那块是 13,正左方那块是 9,这两块把左上角那一小块 6 都算了一遍,重复了,要减掉一次,最后再补上本格自己的 1,得到 17。
- 16算 s[3][3]:它代表从矩阵左上角一直到原矩阵第 2 行第 2 列(值是 2)这块矩形的总和。正上方那块是 15,正左方那块是 17,这两块把左上角那一小块 13 都算了一遍,重复了,要减掉一次,最后再补上本格自己的 2,得到 21。
- 17算 s[3][4]:它代表从矩阵左上角一直到原矩阵第 2 行第 3 列(值是 1)这块矩形的总和。正上方那块是 17,正左方那块是 21,这两块把左上角那一小块 15 都算了一遍,重复了,要减掉一次,最后再补上本格自己的 1,得到 24。
- 18十二个内部格全部填好。现在这张表里,s[i][j] 就是「从矩阵左上角到 (i-1,j-1) 」那块矩形的总和。接下来不再碰原始数字,所有正方形求和都靠这张表的四个角加加减减。边长该取多大?用二分:行列里小的那个是 3,所以边长在 0 到 3 之间,我们二分着试。
- 19二分开始。左端 l 是 0,右端 r 是行列较小值 3。取中点 mid,这里用 (l 加 r 加 1) 右移一位,等于 2;加 1 是让中点往上取整,后面 l 会跳到 mid,不补这个 1 会死循环。现在就去检查:边长 2 的正方形,能不能找到一个和 ≤ 8 的。
- 20检查边长 2,从最靠左上的位置 (0,0) 开始。它的正方形和用四个角算:右下角 s[2][2] 是 13,减掉右上角 s[0][2] 和左下角 s[2][0],再把被减了两次的左上角 s[0][0] 加回来,得到 13。这个值大于 8,这个位置框不下,换右边一个位置。
- 21左上角右移到 (0,1)。同样用四个角:15 减 0 减 6 加 0,等于 9。还是大于 8,继续往右挪。注意四个角的下标都跟着左上角一起平移,这就是 O(1) 的好处,挪一格不用重新加里面的数。
- 22再往右到 (0,2)。四个角:17 减 0 减 13 加 0,等于 4。这回 4 不超过 8,找到了一个合格的边长 2 正方形。只要有一个合格就够了,立刻收手:边长 2 可行。
- 23边长 2 行得通,说明答案至少是 2,我们贪心地想要更大,把左端 l 抬到 2。新区间是 2 到 3,中点 (2 加 3 加 1) 右移一位等于 3。接着去试边长 3 能不能也找到一个和 ≤ 8 的正方形。
- 24检查边长 3,3 行 4 列里能放下 3×3 的左上角只有两个。先看 (0,0):右下角 s[3][3] 是 21,减右上 0、减左下 0、加回左上 0,得 21。远大于 8,框不下。
- 25挪到 (0,1):24 减 0 减 9 加 0,等于 15,还是大于 8。两个位置都失败,边长 3 找不到合格正方形,不可行。
- 26边长 3 不可行,把右端 r 压到 2。现在 l 和 r 都等于 2,区间只剩一个值,二分结束。整个过程里能成立的最大边长就是 2,这就是答案。注意题目要的是边长本身,返回 2,不是面积 4。
- 27把答案放回原矩阵看:就是右上角这个 2×2 区块,左上角在行 0 列 2,盖住行 0 到 1、列 2 到 3,四个数都是 1,加起来 4,不超过 8。它的边长是 2。边长 3 试过框不下,边长 2 框得下,所以最大边长就是 2。
⚠️ 容易写错的地方
✗ 错:每验一个正方形就把里面的数一个个加
✓ 对:先建二维前缀和,任意矩形和用四角公式 O(1) 求
逐格累加会让单次 check 退化到 O(m·n·k²),总复杂度爆炸;前缀和把矩形求和降到常数,是这类题的标配
✗ 错:前缀表和原矩阵一样大、下标直接对应
✓ 对:s 比矩阵大一圈,s[i][j] 对应原矩阵 (i-1,j-1);四角是 s[i+k][j+k] 等
多出的哨兵行列让公式不用写边界特判;下标差一位是这道题最容易错的地方,推公式时盯紧 +1
✗ 错:中点写成 (l 加 r) 右移一位,或返回面积 k×k
✓ 对:中点用 (l 加 r 加 1) 右移一位向上取整;返回边长 k 本身
可行时 l 跳到 mid,中点不向上取整会在区间剩两个数时死循环;且本题问的是最大边长,不是面积,别多乘一次
完整代码(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 maxSideLength(self, mat: List[List[int]], threshold: int) -> int:
def check(k: int) -> bool:
for i in range(m - k + 1):
for j in range(n - k + 1):
v = s[i + k][j + k] - s[i][j + k] - s[i + k][j] + s[i][j]
if v <= threshold:
return True
return False
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
l, r = 0, min(m, n)
while l < r:
mid = (l + r + 1) >> 1
if check(mid):
l = mid
else:
r = mid - 1
return lC++
#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:
int maxSideLength(vector<vector<int>>& mat, int threshold) {
int m = mat.size(), n = mat[0].size();
int s[m + 1][n + 1];
memset(s, 0, sizeof(s));
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] + mat[i - 1][j - 1];
}
}
auto check = [&](int k) {
for (int i = 0; i < m - k + 1; ++i) {
for (int j = 0; j < n - k + 1; ++j) {
if (s[i + k][j + k] - s[i][j + k] - s[i + k][j] + s[i][j] <= threshold) {
return true;
}
}
}
return false;
};
int l = 0, r = min(m, n);
while (l < r) {
int mid = (l + r + 1) >> 1;
if (check(mid)) {
l = mid;
} else {
r = mid - 1;
}
}
return l;
}
};Java
import java.util.*;
class Solution {
private int m;
private int n;
private int threshold;
private int[][] s;
public int maxSideLength(int[][] mat, int threshold) {
m = mat.length;
n = mat[0].length;
this.threshold = threshold;
s = new int[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] + mat[i - 1][j - 1];
}
}
int l = 0, r = Math.min(m, n);
while (l < r) {
int mid = (l + r + 1) >> 1;
if (check(mid)) {
l = mid;
} else {
r = mid - 1;
}
}
return l;
}
private boolean check(int k) {
for (int i = 0; i < m - k + 1; ++i) {
for (int j = 0; j < n - k + 1; ++j) {
if (s[i + k][j + k] - s[i][j + k] - s[i + k][j] + s[i][j] <= threshold) {
return true;
}
}
}
return false;
}
}复杂度
时间
O(m·n·log(min(m,n)))
建前缀表 O(m·n)。二分边长共 O(log(min(m,n))) 次,每次 check 要扫 O(m·n) 个左上角,每个左上角靠前缀表 O(1) 求和。注:也有不二分、直接对每个左上角递增边长的 O(m·n·min(m,n)) 写法,本解走二分更快
空间
O(m·n)
按峰值算:前缀和表 s 是 (m+1)×(n+1),和矩阵同量级。没有递归,不额外吃栈
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 元素和小于等于阈值的正方形的最大边长 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
二维前缀和的表怎么建、怎么查,那圈哨兵 0 有什么用?+
建表:s 比原矩阵大一圈,第 0 行、第 0 列全填 0,内部 s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + mat[i-1][j-1],含义是「从矩阵左上角到 (i-1,j-1)」这块矩形的和。查从 (r1,c1) 到 (r2,c2) 的子矩形和:s[r2+1][c2+1] - s[r1][c2+1] - s[r2+1][c1] + s[r1][c1],都是大矩形减掉上面一条、减掉左边一条,左上角那块被减两次补回一次。哨兵 0 的作用是:最上行、最左列求和时减出去的下标会落到第 0 行列,取到 0 而不是越界,省掉一堆边界特判。
不用二分,直接枚举能做吗?复杂度差多少?+
能。对每个左上角,从当前已知的最大可行边长往大试,因为某个左上角能成边长 k、答案至少 k,下一个左上角就从 k 起试、不回头,这是 O(m·n·min(m,n)) 的滑动枚举。二分是 O(m·n·log(min(m,n))),理论上更快。300×300 的数据两种都能过,面试里把「边长可行性单调」讲清楚,选哪种都站得住。
中点为什么写 (l+r+1)>>1,而不是 (l+r)>>1?+
因为这题找的是最大可行边长,可行分支写 l=mid(把下界抬到 mid,mid 自己可能就是答案,不能跳过)。等区间只剩 l、r 相差 1 时,(l+r)>>1 会算出 l,mid 又赋回 l,区间一格没缩、死循环。加 1 让中点向上取整偏向 r,每轮区间都真在缩。凡是「求满足条件的最大值」型二分、可行时收 l=mid,中点就得 +1 向上取整,这是固定搭配。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 元素和小于等于阈值的正方形的最大边长 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。