LeetCode 221:Maximal Square 动态规划详解

在这篇文章里,通过一道经典题目 Maximal Square(最大正方形),系统梳理这道题的题意、思路演化、核心 DP 转移公式,以及如何直观理解那句"看左、看上、看左上,取最小再加一"。algo+1

题目描述

给定一个 m x n 的二维二进制矩阵 matrix,其中每个元素要么是字符 '0',要么是字符 '1':leetcode

要求在这个矩阵中找到只包含 '1' 的 最大正方形子矩阵

返回这个正方形的 面积(即边长的平方)

示例: sparkcodehub+1

复制代码
matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]

最大正方形边长为 2,面积为 4

复制代码
matrix = [["0","1"],["1","0"]]

最大正方形边长为 1,面积为 1

复制代码
matrix = [["0"]]

没有全为 1 的正方形,面积为 0

**约束:**1 <= m, n <= 300,matrix[i][j] 只能是 '0' 或 '1'。leetcode

直观想法与暴力做法

最直接的想法是"穷举所有可能的正方形":vultr+1

  1. 枚举左上角 (i, j)
  2. 枚举边长 len,检查以 (i, j) 为左上角、边长为 len 的正方形是不是都为 '1'
  3. 更新最大面积

问题在于:

  • 每个起点要尝试很多边长
  • 每个正方形又要检查一堆格子
  • 时间复杂度最坏会接近 O(m²n²)O(m^2 n^2)O(m2n2),在 300 x 300 的数据范围内非常吃力。jointaro+1

于是需要一种方法,把"重复的检查"变成"可复用的状态"------这就是动态规划登场的动机。finalroundai+1

动态规划建模:状态与含义

核心问题:怎么定义一个状态,能帮我们逐步放大正方形? geeksforgeeks+1

非常常见、也是这题最经典的定义是:

令 dp[i][j] 表示:

以 matrix[i][j] 为 右下角 的、全是 '1' 的最大正方形的 边长algo+1

注意几点:

  1. 只考虑以 (i, j) 为 右下角 的正方形,而不是任意包含 (i, j) 的正方形
  2. dp[i][j] 记录的是 边长(如 0、1、2、3...),不是面积
  3. 最终答案是所有 dp[i][j] 中的最大值 maxSide 的平方 maxSide * maxSide。sparkcodehub+1

对于某个位置 (i, j):

  • 如果 matrix[i][j] == '0',那它不可能是任何全 1 正方形的右下角,dp[i][j] = 0。algo+1
  • 如果 matrix[i][j] == '1',才有机会构成更大的正方形。sparkcodehub+1

为什么要看"上、左、左上"三个方向?

这是很多人第一次接触这题时最疑惑的地方:

为啥转移公式是

复制代码
dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1

要看上、左、左上这三个?为什么取最小?finalroundai+1

从 2x2 开始:四个格子全为 1

先从一个最小的非平凡情况------边长为 2 的正方形------想起。finalroundai+1

假设我们想让 (i, j) 成为一个 2x2 正方形 的右下角,这个正方形的四个格子是:

  • (i-1, j-1) 左上
  • (i-1, j) 上
  • (i, j-1) 左
  • (i, j) 自己

要这四个格子全为 '1',(i, j) 本身必须是 1,并且上、左、左上三个也得是 1。algo+1

这里其实已经在暗示:

  • 想要在 (i, j) 形成一个更大的正方形,离不开它周围那三个格子
  • 这三个方向里,只要有一个"不够长",整个正方形就被"卡住"了

放大到任意边长 k:积木叠起来

现在把这个想法推广到边长为 k 的情况。geeksforgeeks+1

想象一下:

  • 以 (i, j) 为右下角的边长为 k 的正方形
  • 它的"上一行"其实是一个以 (i-1, j) 为右下角的、边长至少为 k-1 的正方形"再往下扩一行"
  • 它的"左一列"其实是一个以 (i, j-1) 为右下角的、边长至少为 k-1 的正方形"再往右扩一列"
  • 它的"左上内部"其实是一个以 (i-1, j-1) 为右下角的、边长至少为 k-1 的正方形finalroundai+1

如果把正方形想象成一块块小积木垒出来的,那么:

  • dp[i-1][j] 告诉你:正上方那块区域,最多能形成多大的"正方形塔"
  • dp[i][j-1] 告诉你:正左方那块区域,最多能形成多大的"正方形塔"
  • dp[i-1][j-1] 告诉你:左上那块区域,最多能形成多大的"正方形塔"algo+1

要在 (i, j) 再往外扩一圈,三边都得"跟得上",不然长出来的形状要么缺角,要么变矩形。finalroundai+1

所以:

  • 这三者中"最短"的那一条,决定了你最多还能往外扩多少
  • 因此取 min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) 作为"瓶颈值",再在 (i, j) 这个 1 的基础上 +1。algo+1

于是得到了经典转移方程:

若 matrix[i][j] == '1':

复制代码
dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1

若 matrix[i][j] == '0':

复制代码
dp[i][j] = 0

这一点在很多题解中都有类似表述:"当前格子的最大正方形边长,受限于上方、左方和左上方三个位置的最小值"。finalroundai+1

边界处理:第一行、第一列怎么搞?

转移公式里用了 i-1 和 j-1,要小心下标越界问题。leetcode+1

常见做法有两种:

  1. 直接用大小为 m x n 的 dp 数组

    第一行、第一列单独处理:

    • 如果 matrix[i][j] == '1',则 dp[i][j] = 1(因为只能形成边长 1 的小正方形)
    • 从第二行、第二列开始再用通用转移公式。zhenyu0519.github+1
  2. 用大小为 (m+1) x (n+1) 的 dp 数组

    令 dp[i+1][j+1] 对应 matrix[i][j]

    这样 dp[0][] 和 dp[][0] 都是 0,天然当作"越界之外",写转移时就可以统一写成:

    复制代码
    dp[i+1][j+1] = min(dp[i][j+1], dp[i+1][j], dp[i][j]) + 1

    不需要任何特别的 if 判断,代码更简洁。sparkcodehub+1

无论哪种方式,本质上都是为了解决"如何从边界开始递推"的工程问题,与 DP 思想本身无关。leetcode+1

算法步骤小结

整体流程可以概括为:sparkcodehub+1

  1. 如果矩阵为空,直接返回 0
  2. 初始化 dp 表(m x n 或 (m+1) x (n+1) 二选一)
  3. 用一个变量 maxSide 记录遍历过程中遇到的最大边长
  4. 遍历每个 matrix[i][j]:
    • 若 matrix[i][j] == '0', dp[i][j] = 0
    • 若 matrix[i][j] == '1',按转移式计算: dp[i][j] = min(上,左,左上) + 1
    • 同时用 maxSide = max(maxSide, dp[i][j])
  5. 遍历结束后返回 maxSide * maxSide 作为最终答案

时间复杂度为 O(mn),空间复杂度为 O(mn),也可以通过滚动数组或一维 DP 优化到 O(n) 空间。zhenyu0519.github+1

小例子手推,加深"min + 1"理解

以前面的示例矩阵为例:upe-profdev.gitlab+1

复制代码
1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0

构造同大小的 dp 表(这里省略中间所有步骤,只看关键位置):designgurus+1

在 (2, 2) 这个格子(值为 '1' 的那个位置)计算 dp[2][2] 时:

  • 它的上、左、左上三个 dp 值分别是 1, 1, 1
  • 说明在这三个方向上,当前都至少能有边长 1 的小正方形
  • 所以可以在这个基础上向外扩一圈,形成边长为 1 + 1 = 2 的正方形
  • 得到 dp[2][2] = 2。designgurus+1

再往右扩,某些位置的三个邻居可能是 2, 2, 1,那最小值是 1,只能扩到 1 + 1 = 2;如果某个方向已经是 0,则最小值是 0,只能扩到 1,甚至更小。upe-profdev.gitlab+1

这正是"被最短的那一条边限制住"的直观体现。upe-profdev.gitlab+1

写在最后:如何从这题迁移到别的 DP 题?

这道题的价值不只是写出一个通过的答案,更重要的是:习惯用"以谁为结尾/为右下角"来定义状态。hellointerview+1

这里有几个可以继续思考和练习的方向:

  1. 能否自己实现一维滚动数组版本,把空间压到 O(n)?sparkcodehub+1

  2. 能否类比这题,去看"最大矩形(Maximal Rectangle)"、"最大全 1 子矩阵"等题?sparkcodehub+1

  3. 能否把"上、左、左上取 min + 1"这个模式,总结成一种"从相邻子问题扩张目标形状"的通用套路?hellointerview+1

相关推荐
黑符石15 小时前
【论文研读】Madgwick 姿态滤波算法报告总结
人工智能·算法·机器学习·imu·惯性动捕·madgwick·姿态滤波
源代码•宸15 小时前
Leetcode—39. 组合总和【中等】
经验分享·算法·leetcode·golang·sort·slices
好易学·数据结构15 小时前
可视化图解算法77:零钱兑换(兑换零钱)
数据结构·算法·leetcode·动态规划·力扣·牛客网
AlenTech16 小时前
226. 翻转二叉树 - 力扣(LeetCode)
算法·leetcode·职场和发展
Tisfy16 小时前
LeetCode 1458.两个子序列的最大点积:动态规划
算法·leetcode·动态规划·题解·dp
求梦82016 小时前
【力扣hot100题】合并区间(9)
算法·leetcode·职场和发展
汽车仪器仪表相关领域16 小时前
工况模拟精准检测,合规减排赋能行业 ——NHASM-1 型稳态工况法汽车排气检测系统项目实战经验分享
数据库·算法·单元测试·汽车·压力测试·可用性测试
chilavert31816 小时前
技术演进中的开发沉思-299 计算机原理:数据结构
算法·计算机原理
C+-C资深大佬16 小时前
C++逻辑运算
开发语言·c++·算法