最大的以 1 为边界的正方形 图解题解
这道题到底在问什么
- 输入
- grid = [[1,1,1],[1,0,1],[1,1,1]]
- 输出
- 9(边框整圈是 1,中间那个 0 不影响)
- 输入
- grid = [[1,1,0,0]]
- 输出
- 1(只能凑出一个 1×1)
- 输入
- 本节演示 4×4 网格
- 输出
- 9
最优解:为什么这么做
一句话答案:LeetCode 1139 最大的以 1 为边界的正方形:四条边全 1、中间随意。预处理每格向下、向右连续 1 的长度,再从大到小枚举边长,靠四个角的计数 O(1) 查边框,时间 O(m·n·min(m,n))、空间 O(m·n)。
以 1 为边界是什么意思,返回的是边长还是面积
给一张只装 0 和 1 的网格,找一个子正方形,要它四条边整圈全是 1,框里是 0 还是 1 不管。返回格子总数,边长 k 就返回 k 乘 k,不是 k。样例 grid = [[1,1,1],[1,0,1],[1,1,1]] 输出 9,中间那 0 落在框内、不碰边框。
边框全 1 逐格去数,一张 100×100 的网格要数多少遍
光候选正方形就有 O(n³)(大 O 记号,量级估计)个:每个左上角配上 1 到 min(m, n) 的一种边长。每个还得沿四条边逐格看全不全 1,约 4k 步再叠 O(n),合起来近 O(n⁴)。相邻正方形的边大段重叠、同格反复重数。
把「这条边全是 1 吗」预先算成一个数,查边框就不再走格子
先给每格记两个数:向下、向右各连续几个 1,存进 down 和 right 两张表。两表用小递推(拿邻格结果推出当前格)自底向上、从右往左填(从最下一行、最右一列起倒推,算到当前格时下邻、右邻都已备好):当前格 (i, j)(即(行号,列号),从 0 数起)是 1 时,down[i][j] 等于下邻 down 加 1、right[i][j] 等于右邻 right 加 1;是 0 就记 0,链子在此断。这就是动态规划(每格连续 1 的长度算好存表,查边框直接取)。
一条边到底看哪个角的哪张表,套错角会怎样
判断以 (i, j) 为左上角、边长 k 的正方形,四条边各查一个角、都 O(1):上边看左上角 right[i][j]、左边看左上角 down[i][j],下边看左下角 (i+k-1, j) 的 right、右边看右上角 (i, j+k-1) 的 down,四个都不小于 k 边框才全 1。最易套错的是把下边、右边也按左上角查,认错角就查了条不存在的边。
拿演示的 4×4 网格,把两张表填出来再枚举边长
演示网格 4×4,(0,0)、(3,3) 是 0,其余十四格全 1,同规则填两表。第 0 行的 right 从右往左:(0,3) 到头记 1,(0,2) 加 1 得 2,(0,1) 再加 1 得 3,(0,0) 遇 0 断成 0;第 1 列的 down 从下往上:(3,1) 记 1、(2,1) 得 2、(1,1) 得 3、(0,1) 得 4;第 2 行、第 3 列同理给出 (2,1) right=3、(0,3) down=3。枚举边长:4 的唯一起点 (0,0) 是 0 出局;缩到 3,别的左上角也这样查四角,这里演代表 (0,1):上左下右依次读 right[0][1]=3、down[0][1]=4、right[2][1]=3、down[0][3]=3,都≥3 全过,返回 3 乘 3 等于 9。
下边误看成左上角的计数,一个本该 9 的答案为什么会缩水
填两表扫一遍网格 O(m·n);枚举边长 min(m, n) 种、每种全网格 O(1) 查边,方阵约 O(n³);两表占 O(m·n) 空间。返回值写成边长 k 而非面积 k 乘 k,样例 9 会缩成 3。起点角认错,答案就莫名偏小。以为框里也得全 1、把带 0 的 9 判掉——其实只看边框这圈。边界:中间 0 仍算 9,单行最多 1×1,全 0 返回 0。
▶ 动画逐步走查(共 26 步)——想跟着动画一帧帧对照就展开
- 3记住这条:把每个格子向下、向右的连续 1 个数先算好,四条边是否全 1 就靠四个角的计数和 k 比大小。边长从大到小,首次命中即最大。
- 4这是一张 4×4 网格,行号 i、列号 j。左上角 (0,0) 和右下角 (3,3) 是 0,其余十四格都是 1。还没处理的格子先显示原始的 0 或 1,处理过的格子会换成「向下连续数、向右连续数」这两个值。我们要找的最大正方形,答案会落在 9。
- 5第 3 行第 3 列是 0,连续 1 的链子在这里断掉,向下、向右都记 0。
- 6第 3 行第 2 列是 1。下面已经到头,下面没有格子(算作 0),所以向下只算自己这 1 个;向右接着右边那格的 0 个,连上自己向右连续 1 个。
- 7第 3 行第 1 列是 1。下面已经到头,下面没有格子(算作 0),所以向下只算自己这 1 个;向右接着右边那格的 1 个,连上自己向右连续 2 个。
- 8第 3 行第 0 列是 1。下面已经到头,下面没有格子(算作 0),所以向下只算自己这 1 个;向右接着右边那格的 2 个,连上自己向右连续 3 个。
- 9第 2 行第 3 列是 1。向下接着下面那格的 0 个,连上自己向下连续 1 个;右边已经到头,右边没有格子(算作 0),所以向右只算自己这 1 个。
- 10第 2 行第 2 列是 1。向下接着下面那格的 1 个,连上自己向下连续 2 个;向右接着右边那格的 1 个,连上自己向右连续 2 个。
- 11第 2 行第 1 列是 1。向下接着下面那格的 1 个,连上自己向下连续 2 个;向右接着右边那格的 2 个,连上自己向右连续 3 个。
- 12第 2 行第 0 列是 1。向下接着下面那格的 1 个,连上自己向下连续 2 个;向右接着右边那格的 3 个,连上自己向右连续 4 个。
- 13第 1 行第 3 列是 1。向下接着下面那格的 1 个,连上自己向下连续 2 个;右边已经到头,右边没有格子(算作 0),所以向右只算自己这 1 个。
- 14第 1 行第 2 列是 1。向下接着下面那格的 2 个,连上自己向下连续 3 个;向右接着右边那格的 1 个,连上自己向右连续 2 个。
- 15第 1 行第 1 列是 1。向下接着下面那格的 2 个,连上自己向下连续 3 个;向右接着右边那格的 2 个,连上自己向右连续 3 个。
- 16第 1 行第 0 列是 1。向下接着下面那格的 2 个,连上自己向下连续 3 个;向右接着右边那格的 3 个,连上自己向右连续 4 个。
- 17第 0 行第 3 列是 1。向下接着下面那格的 2 个,连上自己向下连续 3 个;右边已经到头,右边没有格子(算作 0),所以向右只算自己这 1 个。
- 18第 0 行第 2 列是 1。向下接着下面那格的 3 个,连上自己向下连续 4 个;向右接着右边那格的 1 个,连上自己向右连续 2 个。
- 19第 0 行第 1 列是 1。向下接着下面那格的 3 个,连上自己向下连续 4 个;向右接着右边那格的 2 个,连上自己向右连续 3 个。
- 20第 0 行第 0 列是 0,连续 1 的链子在这里断掉,向下、向右都记 0。
- 21十六个格子全部处理完。现在每格都写着向下、向右两个连续 1 的个数,0 的格子是 ↓0 →0。接下来从最大的边长开始,看看能不能拼出一个四边全 1 的正方形。
- 22先试最大的边长 4,整张网格只有一个左上角 (0,0)。可它本身就是 0,向右、向下的连续计数都是 0,上边都凑不齐,边长 4 直接出局。
- 23边长缩到 3。第一个左上角还是 (0,0),它是 0,连第一条边都开不了头,跳过,换下一个左上角。
- 24换到左上角 (0,1),它是 1,可以开工。先查上边,这条边占第 0 行的列 1 到 3,正好是从 (0,1) 向右数 3 格。看它的向右计数 right 是 3,不小于 3,说明这 3 格全是 1,上边过关。
- 25再查左边,这条边占第 1 列的行 0 到 2,是从 (0,1) 向下数 3 格。看它的向下计数 down 是 4,比 3 还多,这 3 格当然全是 1,左边也过关。
- 26接着查下边。下边的起点是左下角 (2,1),这条边从它向右数 3 格。看 (2,1) 的向右计数 right 是 3,不小于 3,下边全是 1,过关。注意下边看的是左下角,别看错成左上角。
- 27最后查右边。右边的起点是右上角 (0,3),从它向下数 3 格。看 (0,3) 的向下计数 down 是 3,正好够 3,右边全是 1。四条边全部通过,边长 3 的正方形找到了。
- 28把这圈边框点亮:左上角 (0,1),覆盖第 0 到 2 行、第 1 到 3 列。整圈 8 个边框格全是 1,里面那格是什么都无所谓。因为我们是从大边长往小试的,4 凑不出、3 第一个命中,这个 3 就是最大的。它覆盖的 9 个格子面积是 3 乘 3 等于 9,这就是答案。
⚠️ 容易写错的地方
✗ 错:以为正方形内部也必须全是 1
✓ 对:只要四条边整圈是 1,内部是 0 也算
题目要的是「以 1 为边界」,经典样例中间就是 0 照样返回 9;判断时只查边框,别去管内部
✗ 错:返回边长 k,或从小边长往大枚举
✓ 对:返回面积 k×k;边长从大往小枚举,第一个命中即最大
要的是元素个数,所以是 k 乘 k;从大往小试能在首次命中时直接返回,省去比较谁更大
✗ 错:四条边都拿左上角的计数去查
✓ 对:上、左看左上角,下边看左下角,右边看右上角
一条边全 1 要由它「起点角」的连续计数保证:下边起点是左下角 (i+k-1,j),右边起点是右上角 (i,j+k-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 largest1BorderedSquare(self, grid: List[List[int]]) -> int:
m, n = len(grid), len(grid[0])
down = [[0] * n for _ in range(m)]
right = [[0] * n for _ in range(m)]
for i in range(m - 1, -1, -1):
for j in range(n - 1, -1, -1):
if grid[i][j]:
down[i][j] = down[i + 1][j] + 1 if i + 1 < m else 1
right[i][j] = right[i][j + 1] + 1 if j + 1 < n else 1
for k in range(min(m, n), 0, -1):
for i in range(m - k + 1):
for j in range(n - k + 1):
if (
down[i][j] >= k
and right[i][j] >= k
and right[i + k - 1][j] >= k
and down[i][j + k - 1] >= k
):
return k * k
return 0C++
#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 largest1BorderedSquare(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
int down[m][n];
int right[m][n];
memset(down, 0, sizeof down);
memset(right, 0, sizeof right);
for (int i = m - 1; i >= 0; --i) {
for (int j = n - 1; j >= 0; --j) {
if (grid[i][j] == 1) {
down[i][j] = i + 1 < m ? down[i + 1][j] + 1 : 1;
right[i][j] = j + 1 < n ? right[i][j + 1] + 1 : 1;
}
}
}
for (int k = min(m, n); k > 0; --k) {
for (int i = 0; i <= m - k; ++i) {
for (int j = 0; j <= n - k; ++j) {
if (down[i][j] >= k && right[i][j] >= k && right[i + k - 1][j] >= k
&& down[i][j + k - 1] >= k) {
return k * k;
}
}
}
}
return 0;
}
};Java
import java.util.*;
class Solution {
public int largest1BorderedSquare(int[][] grid) {
int m = grid.length, n = grid[0].length;
int[][] down = new int[m][n];
int[][] right = new int[m][n];
for (int i = m - 1; i >= 0; --i) {
for (int j = n - 1; j >= 0; --j) {
if (grid[i][j] == 1) {
down[i][j] = i + 1 < m ? down[i + 1][j] + 1 : 1;
right[i][j] = j + 1 < n ? right[i][j + 1] + 1 : 1;
}
}
}
for (int k = Math.min(m, n); k > 0; --k) {
for (int i = 0; i <= m - k; ++i) {
for (int j = 0; j <= n - k; ++j) {
if (down[i][j] >= k && right[i][j] >= k && right[i + k - 1][j] >= k
&& down[i][j + k - 1] >= k) {
return k * k;
}
}
}
}
return 0;
}
}复杂度
时间
O(m·n·min(m,n))
预处理两张表是 O(m·n)。枚举阶段:边长 k 有 min(m,n) 种,每种要扫 O(m·n) 个左上角,每个左上角靠预处理的计数 O(1) 查四条边。方阵时约为 O(n³),n ≤ 100 完全够用
空间
O(m·n)
按峰值算:down、right 两张和网格同样大的表,都是 m×n,空间是 O(m·n);若网格是 n×n 方阵就是 O(n²)。没有递归,不额外吃栈
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 最大的以 1 为边界的正方形 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么下边要看左下角、右边要看右上角,不能都用左上角?+
因为 down[i][j] 只记录从 (i, j) 这一点向下的连续 1,right 只记录向右的连续 1。正方形的下边整条躺在最底那行,它的最左格是左下角 (i+k-1, j),从这里向右数 k 格才覆盖整条下边,所以查的是左下角的 right。右边整条在最右那列,最上格是右上角 (i, j+k-1),从这里向下数 k 格覆盖右边,查的是右上角的 down。上边、左边都从左上角出发,正好一个向右一个向下。四条边各由它自己的起点角负责,套到左上角头上就会去查一条不存在的边。
为什么边长要从大到小枚举,从小到大不行吗?+
从小到大也能算对,但你得一直记着见过的最大边长、扫完整张表才能定答案。从大到小枚举时,第一个凑齐四条边的正方形一定是最大的,当场返回、省去后续所有更小边长的扫描和比较。两种都是 O(n³) 量级,从大到小只是靠早停少跑一些。
这题和最大正方形 LeetCode 221(要求实心全 1)有什么区别?+
221 要正方形内部每一格都是 1,用的是 dp[i][j]=min(左、上、左上)+1 这条经典递推,直接算实心正方形的最大边长。本题只要求四条边全 1、内部随意,实心那条递推管不住「空心也算」,所以换成预处理向下、向右两张连续 1 的计数表,靠四个角查边框。一个盯整块面积、一个只盯一圈边框,模型完全不同。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 最大的以 1 为边界的正方形 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。