跳至主要內容

最大正方形

周子力大约 7 分钟教学文档动态规划-二维动态规划

题目描述

picture 0
picture 0

解题思路

这是一个经典的 二维动态规划(2D DP)问题,通常称为 “最大正方形”(Maximal Square)。


✅ 问题分析

我们要在由 '0''1' 组成的矩阵中,找到全为 '1' 的最大正方形,并返回其面积(即边长的平方)。

关键点:

  • 正方形必须连续且全为 '1'
  • 返回的是面积,但我们可以先求最大边长,再平方。

✅ 动态规划思路

状态定义:

dp[i][j] 表示 (i, j) 为右下角的最大正方形的边长

为什么选“右下角”?因为这样可以利用左、上、左上三个方向的信息来扩展正方形。

状态转移:

  • 如果 matrix[i][j] == '0',则无法形成全 '1' 正方形 → dp[i][j] = 0
  • 如果 matrix[i][j] == '1',则:
    dp[i][j] = 1 + min(
        dp[i-1][j],     # 上方
        dp[i][j-1],     # 左方
        dp[i-1][j-1]    # 左上方
    )
    
    解释:当前点能扩展成多大的正方形,受限于三个邻居中最小的那个(木桶效应)。

初始化:

  • 第一行和第一列:若 matrix[i][j] == '1',则 dp[i][j] = 1
  • 可以在主循环中通过判断 i == 0 or j == 0 处理,也可以提前初始化。

答案:

  • 遍历过程中记录 max_side = max(max_side, dp[i][j])
  • 最终返回 max_side * max_side

✅ Python 代码实现

def maximalSquare(matrix):
    if not matrix or not matrix[0]:
        return 0
    
    m, n = len(matrix), len(matrix[0])
    dp = [[0] * n for _ in range(m)]
    max_side = 0

    for i in range(m):
        for j in range(n):
            if matrix[i][j] == '1':
                if i == 0 or j == 0:
                    dp[i][j] = 1
                else:
                    dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
                max_side = max(max_side, dp[i][j])
            # else: dp[i][j] 保持为 0
    
    return max_side * max_side

✅ 示例验证

# 示例 1
matrix1 = [
    ["1","0","1","0","0"],
    ["1","0","1","1","1"],
    ["1","1","1","1","1"],
    ["1","0","0","1","0"]
]
print(maximalSquare(matrix1))  # 输出: 4

# 示例 2
matrix2 = [["0","1"],["1","0"]]
print(maximalSquare(matrix2))  # 输出: 1

# 示例 3
matrix3 = [["0"]]
print(maximalSquare(matrix3))  # 输出: 0

全部符合预期。


⏱️ 复杂度分析

  • 时间复杂度:O(m × n),每个格子访问一次。
  • 空间复杂度:O(m × n),用于 DP 表。

可进一步优化空间至 O(n)(只保存上一行),但本题 m, n ≤ 300,无需优化。


✅ 关键启发(与 LCS 对比)

问题状态含义依赖方向核心思想
LCS两个序列前缀的匹配长度左、上、左上序列对齐
Maximal Square以 (i,j) 为右下角的最大正方形边长左、上、左上木桶效应 + 局部扩展

虽然都用 minmax 和三个方向,但语义完全不同:LCS 是“选择最优路径”,而本题是“受限扩展”。


✅ 此解法简洁高效,是处理矩阵中形状探测类问题的标准 DP 模板之一。

非常好的问题!“最大正方形”(Maximal Square)虽然是一个经典 DP 题,但其中蕴含的细节和思想非常值得深入挖掘。下面从 实现注意事项算法启发 两个维度进行系统总结。


一、代码实现中的注意事项(易错点 & 实践细节)

1. 字符 '1' 与整数 1 的混淆

  • 输入矩阵中元素是字符串 '0''1',不是整数。
  • ❌ 错误写法:if matrix[i][j] == 1:
  • ✅ 正确写法:if matrix[i][j] == '1':

这是 LeetCode 上高频出错点,尤其在快速编码时容易忽略。


2. 边界条件的处理方式

  • i == 0j == 0(第一行或第一列)时,无法访问 dp[i-1][j-1] 等。
  • 解决方案有两种:
    • 方案 A:在循环内判断 if i == 0 or j == 0: dp[i][j] = 1
    • 方案 B:创建 (m+1) x (n+1) 的 DP 表,从 i=1, j=1 开始遍历,避免越界。

✅ 推荐方案 A(更直观),但需确保逻辑覆盖所有边界。


3. DP 表初始化为 0 是安全的

  • 因为 '0' 对应的 dp[i][j] 就是 0,无需额外赋值。
  • 所有 '1' 的位置会在主循环中被正确更新。

这种“懒初始化”简化了代码,但前提是状态转移逻辑覆盖所有情况。


4. 不要忘记更新全局最大值

  • dp[i][j] 只表示以 (i,j) 为右下角的正方形边长。
  • 最终答案是所有 dp[i][j] 中的最大值的平方。
  • ❌ 常见错误:直接返回 dp[m-1][n-1] ** 2(只考虑右下角)。

✅ 必须在循环中维护 max_side = max(max_side, dp[i][j])


5. 空输入的防御性检查

  • 虽然题目保证 m, n ≥ 1,但在实际工程或面试中,建议加:
    if not matrix or not matrix[0]:
        return 0
    
  • 体现代码鲁棒性。

6. 空间优化的可能性(进阶)

  • 由于 dp[i][j] 只依赖上一行和当前行的左侧值,可用滚动数组优化到 O(n) 空间:
    prev = [0] * n
    curr = [0] * n
    for i in range(m):
        for j in range(n):
            if matrix[i][j] == '1':
                if i == 0 or j == 0:
                    curr[j] = 1
                else:
                    curr[j] = 1 + min(prev[j], curr[j-1], prev[j-1])
                max_side = max(max_side, curr[j])
            else:
                curr[j] = 0
        prev, curr = curr, [0] * n  # 或复用 curr 并清零
    
  • 在内存敏感场景有用,但本题无需。

二、由本题得到的算法启发

1. “以某点为右下角”是矩阵形状问题的经典状态设计

  • 类似思路也用于:
    • 最大矩形(需结合柱状图 + 单调栈)
    • 全 1 子矩阵计数
    • 岛屿问题中的局部结构分析
  • 核心思想:将全局问题分解为每个位置作为“终点”的局部问题

✅ 启发:遇到“最大/最小形状”问题,优先考虑“以 (i,j) 为角点”的状态定义。


2. 木桶原理在 DP 中的体现

  • 当前正方形边长受限于 上、左、左上 三个方向的最小值。
  • 这是因为要形成边长为 k 的正方形,三个方向都必须至少有 k-1 的边长

这是典型的“短板效应”在算法中的应用。

✅ 启发:当多个条件必须同时满足时,状态转移常取 min;当只需满足其一时,常取 max


3. 与“最大全 1 矩形”的区别

  • 正方形:边长相等 → 可用上述 DP,O(mn)
  • 矩形:长宽可不同 → 不能直接用此 DP,需对每行构建高度数组 + 单调栈(如 LeetCode 85 题)

✅ 启发:形状约束越强,越可能有高效 DP 解法;约束越弱(如任意矩形),可能需要更复杂的数据结构。


4. DP 状态的“局部性”与“可扩展性”

  • 本题的 dp[i][j]局部最优(以 (i,j) 为右下角),但通过遍历所有位置,可得到全局最优
  • 这体现了 DP 的核心哲学:通过局部最优解的组合,构造全局最优解

✅ 启发:设计状态时,问自己:“如果我知道周围的信息,我能推出当前位置的最优解吗?”


5. 从暴力到 DP 的思维跃迁

  • 暴力解法:枚举所有可能的正方形(O(m²n²)),检查是否全为 '1'
  • DP 解法:利用重叠子问题和最优子结构,将复杂度降至 O(mn)。

✅ 启发:DP 的本质是避免重复计算。当你发现暴力解中有大量重复子问题时,考虑 DP。


6. 与图像处理的联系

  • 在计算机视觉中,类似思想用于:
    • 检测图像中的方形区域
    • 形态学操作(如膨胀、腐蚀)
    • 特征提取(如 Haar 特征)
  • 虽然实际系统用更高效的方法(如积分图),但 DP 提供了理论基础。

✅ 启发:基础算法往往是高级应用的基石。


总结

维度关键点
实现注意字符比较、边界处理、全局最大值更新、空输入检查
算法启发右下角状态设计、木桶原理、形状约束与解法复杂度、局部到全局的 DP 思想

掌握这些细节和思想,不仅能正确高效地解决“最大正方形”问题,还能迁移到更广泛的矩阵 DP 和形状分析问题中。

上次编辑于:
贡献者: zilizhou