# 最大正方形 (Maximal Square) LeetCode 221. Medium ## 题目描述 在一个由 '0' 和 '1' 组成的二维矩阵内,找到只包含 '1' 的最大正方形,并返回其面积。 **示例 1**: ``` 输入:matrix = [ ["1","0","1","0","0"], ["1","0","1","1","1"], ["1","1","1","1","1"], ["1","0","0","1","0"] ] 输出:4 ``` **示例 2**: ``` 输入:matrix = [["0","1"],["1","0"]] 输出:1 ``` ## 思路推导 ### 暴力解法分析 **最直观的思路**:枚举所有可能的正方形 ```go func maximalSquare(matrix [][]byte) int { if len(matrix) == 0 { return 0 } m, n := len(matrix), len(matrix[0]) maxSide := 0 // 枚举每个位置作为左上角 for i := 0; i < m; i++ { for j := 0; j < n; j++ { if matrix[i][j] == '1' { // 枚举可能的边长 for side := 1; side <= min(m-i, n-j); side++ { // 检查边长为 side 的正方形是否全为 1 if isValidSquare(matrix, i, j, side) { maxSide = max(maxSide, side) } } } } } return maxSide * maxSide } func isValidSquare(matrix [][]byte, x, y, side int) bool { for i := x; i < x+side; i++ { for j := y; j < y+side; j++ { if matrix[i][j] != '1' { return false } } } return true } ``` **时间复杂度分析**: ``` - 外层循环:O(m × n) 枚举左上角 - 中层循环:O(min(m, n)) 枚举边长 - 内层检查:O(side²) 检查正方形 - 总计:O(m × n × min(m, n) × min(m, n)²) = O(m² × n²) ``` **问题**:复杂度过高,对于大型矩阵会超时。 ### 优化思考 **观察**:暴力解法中,很多检查是重复的。 **关键问题**:能否利用已计算的信息避免重复检查? **思路1:前缀和优化** ```go // 计算每个位置为右下角的矩形中 1 的个数 // 可以 O(1) 判断一个矩形是否全为 1 ``` **时间复杂度**:O(m × n × min(m, n)) - 仍然不够好 **思路2:动态规划** ✅ 核心洞察:**以 (i, j) 为右下角的最大正方形,取决于其相邻三个位置的状态** ### 为什么这样思考? **关键观察**: ``` 如果要形成以 (i, j) 为右下角的正方形,需要满足: 1. 位置 (i, j) 本身是 '1' 2. 其左边、上边、左上三个位置都能形成足够大的正方形 图示: (i-1, j-1) (i-1, j) ┌─────┐ │ │ └─────┼─ (i, j) (i, j-1) 如果 (i, j) 是 '1',那么: - 以 (i, j) 为右下角的最大正方形边长 - = min(上边、左边、左上边的最大正方形边长) + 1 ``` **为什么取 min?** - 只有当三个方向都能形成 k×k 的正方形时 - 当前位置才能形成 (k+1)×(k+1) 的正方形 - 这是"木桶效应":受限于最短的那块木板 ## 解题思路 ### 核心思想 **动态规划**: - **状态定义**:dp[i][j] 表示以 matrix[i-1][j-1] 为右下角的最大正方形边长 - **状态转移**:dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 - **边界条件**:第一行和第一列的 dp 值等于矩阵本身的值 ### 算法流程(详细版) **步骤1:定义 DP 数组** ```go m, n := len(matrix), len(matrix[0]) dp := make([][]int, m+1) // 多一行一列,方便处理边界 for i := range dp { dp[i] = make([]int, n+1) } ``` **为什么用 m+1 和 n+1?** - dp[i][j] 对应矩阵中的 matrix[i-1][j-1] - 这样第一行和第一列自然为 0,作为边界条件 - 避免在循环中特殊判断 i=0 或 j=0 的情况 **步骤2:遍历矩阵,填充 DP 表** ```go maxSide := 0 for i := 1; i <= m; i++ { for j := 1; j <= n; j++ { if matrix[i-1][j-1] == '1' { // 当前位置是 '1',计算能形成的最大正方形 dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 if dp[i][j] > maxSide { maxSide = dp[i][j] } } // 如果是 '0',dp[i][j] 默认为 0 } } ``` **关键点解释**: **Q1:为什么是 dp[i-1][j], dp[i][j-1], dp[i-1][j-1]?** ``` 这三个位置分别是: - dp[i-1][j]:上边位置为右下角的最大正方形 - dp[i][j-1]:左边位置为右下角的最大正方形 - dp[i-1][j-1]:左上位置为右下角的最大正方形 只有这三个位置都能形成 k×k 的正方形 当前位置 (i, j) 才能形成 (k+1)×(k+1) 的正方形 ``` **Q2:为什么取 min?** ``` 示例: 1 1 1 1 1 1 1 1 1 假设我们要计算 dp[3][3](右下角): - dp[2][3] = 2 (上边能形成 2×2) - dp[3][2] = 2 (左边能形成 2×2) - dp[2][2] = 2 (左上能形成 2×2) 取 min = 2,所以 dp[3][3] = 2 + 1 = 3 即可以形成 3×3 的正方形 ✓ 反例: 1 1 0 1 1 1 1 1 1 计算 dp[3][3]: - dp[2][3] = 1 (上边只能形成 1×1,因为被 0 限制) - dp[3][2] = 2 - dp[2][2] = 1 取 min = 1,所以 dp[3][3] = 1 + 1 = 2 只能形成 2×2 的正方形 ✓ ``` **Q3:为什么 +1?** ``` +1 表示当前位置本身也是一个 '1' 可以把当前正方形看作: - 在 (i-1, j-1) 位置的正方形基础上 - 向右扩展一列,向下扩展一行 - 再加上当前位置的 '1' ``` **步骤3:返回面积** ```go return maxSide * maxSide ``` ### 关键细节说明 **细节1:为什么 dp 数组要比矩阵大?** ```go // 矩阵:m 行 n 列 // dp 数组:(m+1) 行 (n+1) 列 dp := make([][]int, m+1) // ✅ 正确 for i := range dp { dp[i] = make([]int, n+1) } // 作用: // - dp[0][j] 和 dp[i][0] 都是 0,作为边界 // - dp[i][j] 对应 matrix[i-1][j-1] // - 避免在循环中判断边界条件 ``` **细节2:为什么 matrix[i-1][j-1]?** ```go for i := 1; i <= m; i++ { for j := 1; j <= n; j++ { if matrix[i-1][j-1] == '1' { // 注意索引偏移 // ... } } } // 因为 dp 从 1 开始,但矩阵从 0 开始 // dp[i][j] 对应 matrix[i-1][j-1] ``` **细节3:min 函数的实现** ```go func min(a, b, c int) int { if a < b { if a < c { return a } return c } if b < c { return b } return c } // Go 1.21+ 可以使用内置的 min 函数 // import "cmp" // minVal := min(a, b, c) ``` ### 边界条件分析 **边界1:空矩阵** ``` 输入:matrix = [] 输出:0 处理:开头直接检查 if len(matrix) == 0 ``` **边界2:全 0 矩阵** ``` 输入:matrix = [["0","0"],["0","0"]] 输出:0 处理:所有 dp[i][j] 保持为 0,maxSide = 0 ``` **边界3:单个 1** ``` 输入:matrix = [["1"]] 输出:1 过程: dp[1][1] = min(dp[0][1], dp[1][0], dp[0][0]) + 1 = min(0, 0, 0) + 1 = 1 maxSide = 1 面积 = 1 × 1 = 1 ✓ ``` **边界4:只有一行或一列** ``` 输入:matrix = [["1","1","1"]] 输出:1 过程: dp[1][1] = 1, dp[1][2] = 1, dp[1][3] = 1 maxSide = 1(无法形成更大的正方形) 面积 = 1 × 1 = 1 ✓ ``` ### 复杂度分析(详细版) **时间复杂度**: ``` - 遍历矩阵:O(m × n) - 每个位置的计算:O(1)(只是三次比较和一次加法) - 总计:O(m × n) 为什么是 O(m × n)? - 只需要遍历矩阵一次 - 每个位置的计算都是常数时间 ``` **空间复杂度**: ``` - DP 数组:O((m+1) × (n+1)) = O(m × n) - 其他变量:O(1) - 总计:O(m × n) 能否优化? - 可以优化到 O(n),只保留两行 - 但代码会变复杂,O(m × n) 通常可接受 ``` ## 执行过程演示 **示例矩阵**: ``` matrix = [ ["1","0","1","0","0"], ["1","0","1","1","1"], ["1","1","1","1","1"], ["1","0","0","1","0"] ] DP 表(加了一圈 0 边界): 0 0 0 0 0 0 0 1 0 1 0 0 0 1 0 1 1 1 0 1 1 2 2 2 0 1 0 0 1 0 填充过程: ``` **步骤详解**: ``` i=1, j=1: matrix[0][0]='1' dp[1][1] = min(dp[0][1], dp[1][0], dp[0][0]) + 1 = min(0, 0, 0) + 1 = 1 i=1, j=2: matrix[0][1]='0' dp[1][2] = 0(保持默认值) i=1, j=3: matrix[0][2]='1' dp[1][3] = min(0, 0, 0) + 1 = 1 i=2, j=1: matrix[1][0]='1' dp[2][1] = min(1, 0, 0) + 1 = 1 i=2, j=3: matrix[1][2]='1' dp[2][3] = min(dp[1][3], dp[2][2], dp[1][2]) + 1 = min(1, 0, 0) + 1 = 1 i=3, j=3: matrix[2][2]='1' ← 关键位置 dp[3][3] = min(dp[2][3], dp[3][2], dp[2][2]) + 1 = min(1, 1, 0) + 1 = 1 等等,让我重新检查... 正确的 DP 表: 0 0 0 0 0 0 0 1 0 1 0 0 0 1 0 1 1 1 0 1 1 2 2 2 ← 这里 0 1 0 0 1 0 i=3, j=3: matrix[2][2]='1' dp[3][3] = min(dp[2][3], dp[3][2], dp[2][2]) + 1 = min(1, 1, 0) + 1 = 1 ❌ 错误 让我重新计算... 实际上 dp[2][2] 应该是 0(因为 matrix[1][1]='0') 所以 min(1, 1, 0) + 1 = 1 但答案说最大是 2×2=4... 让我找找哪里是 2... i=3, j=4: matrix[2][3]='1' dp[3][4] = min(dp[2][4], dp[3][3], dp[2][3]) + 1 = min(1, 1, 1) + 1 = 2 ✓ ``` **最大正方形位置**: ``` 以 (2,3) 为右下角(matrix 索引,从 0 开始) 即 dp[3][4] = 2 可以形成 2×2 的正方形: 1 1 1 1 ``` ## 代码实现 ### Go 实现 ```go func maximalSquare(matrix [][]byte) int { if len(matrix) == 0 { return 0 } m, n := len(matrix), len(matrix[0]) // 创建 DP 表,多一行一列作为边界 dp := make([][]int, m+1) for i := range dp { dp[i] = make([]int, n+1) } maxSide := 0 // 填充 DP 表 for i := 1; i <= m; i++ { for j := 1; j <= n; j++ { if matrix[i-1][j-1] == '1' { // 状态转移 dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 // 更新最大边长 if dp[i][j] > maxSide { maxSide = dp[i][j] } } // 如果是 '0',dp[i][j] 保持为 0 } } // 返回面积 return maxSide * maxSide } // 返回三个数中的最小值 func min(a, b, c int) int { if a < b { if a < c { return a } return c } if b < c { return b } return c } ``` ### Go 1.21+ 优化版本 ```go func maximalSquare(matrix [][]byte) int { if len(matrix) == 0 { return 0 } m, n := len(matrix), len(matrix[0]) dp := make([][]int, m+1) for i := range dp { dp[i] = make([]int, n+1) } maxSide := 0 for i := 1; i <= m; i++ { for j := 1; j <= n; j++ { if matrix[i-1][j-1] == '1' { // Go 1.21+ 内置 min 函数 dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 maxSide = max(maxSide, dp[i][j]) } } } return maxSide * maxSide } ``` ## 常见错误 ### 错误1:DP 数组大小错误 ❌ **错误写法**: ```go dp := make([][]int, m) // ❌ 应该是 m+1 for i := range dp { dp[i] = make([]int, n) // ❌ 应该是 n+1 } ``` ✅ **正确写法**: ```go dp := make([][]int, m+1) // ✓ 多一行 for i := range dp { dp[i] = make([]int, n+1) // ✓ 多一列 } ``` **原因**: - 需要 dp[0][*] 和 dp[*][0] 作为边界(值为 0) - 方便处理第一行和第一列的情况 ### 错误2:索引偏移错误 ❌ **错误写法**: ```go for i := 0; i < m; i++ { // ❌ 索引不对应 for j := 0; j < n; j++ { if matrix[i][j] == '1' { dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 // 当 i=0 或 j=0 时会越界! } } } ``` ✅ **正确写法**: ```go for i := 1; i <= m; i++ { // ✓ 从 1 开始 for j := 1; j <= n; j++ { if matrix[i-1][j-1] == '1' { // ✓ 索引对应 dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 } } } ``` **原因**: - dp 从索引 1 开始,避免越界 - matrix[i-1][j-1] 对应 dp[i][j] ### 错误3:忘记检查空矩阵 ❌ **错误写法**: ```go func maximalSquare(matrix [][]byte) int { m, n := len(matrix), len(matrix[0]) // 空矩阵会 panic! // ... } ``` ✅ **正确写法**: ```go func maximalSquare(matrix [][]byte) int { if len(matrix) == 0 { // ✓ 先检查 return 0 } m, n := len(matrix), len(matrix[0]) // ... } ``` **原因**: - 空矩阵访问 matrix[0] 会 panic - 需要提前处理边界情况 ### 错误4:min 函数实现错误 ❌ **错误写法**: ```go func min(a, b, c int) int { if a < b && a < c { // ❌ 逻辑错误 return a } if b < c { return b } return c } ``` ✅ **正确写法**: ```go func min(a, b, c int) int { if a < b { if a < c { return a } return c } if b < c { return b } return c } ``` **原因**: - 嵌套判断确保正确找到最小值 - 第一个条件应该先比较 a 和 b ## 进阶问题 ### Q1:空间复杂度能否优化? **方案**:只保留两行 DP 值 ```go func maximalSquare(matrix [][]byte) int { if len(matrix) == 0 { return 0 } m, n := len(matrix), len(matrix[0]) // 只需要两行 prev := make([]int, n+1) curr := make([]int, n+1) maxSide := 0 for i := 1; i <= m; i++ { for j := 1; j <= n; j++ { if matrix[i-1][j-1] == '1' { curr[j] = min(prev[j], curr[j-1], prev[j-1]) + 1 maxSide = max(maxSide, curr[j]) } else { curr[j] = 0 } } // 交换行 prev, curr = curr, prev } return maxSide * maxSide } ``` **空间复杂度**:O(n)(从 O(m×n) 优化到 O(n)) ### Q2:如果需要返回最大正方形的坐标怎么办? **方案**:记录最大正方形的右下角位置 ```go func maximalSquareWithPosition(matrix [][]byte) (int, int, int) { if len(matrix) == 0 { return 0, -1, -1 } m, n := len(matrix), len(matrix[0]) dp := make([][]int, m+1) for i := range dp { dp[i] = make([]int, n+1) } maxSide := 0 maxX, maxY := -1, -1 for i := 1; i <= m; i++ { for j := 1; j <= n; j++ { if matrix[i-1][j-1] == '1' { dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 if dp[i][j] > maxSide { maxSide = dp[i][j] maxX, maxY = i-1, j-1 // 转换为矩阵坐标 } } } } return maxSide * maxSide, maxX, maxY } ``` ### Q3:如何找到所有的最大正方形? **方案**:先找到最大边长,再遍历 DP 表找出所有位置 ```go func allMaximalSquares(matrix [][]byte) [][]int { if len(matrix) == 0 { return nil } m, n := len(matrix), len(matrix[0]) dp := make([][]int, m+1) for i := range dp { dp[i] = make([]int, n+1) } maxSide := 0 // 第一遍:找到最大边长 for i := 1; i <= m; i++ { for j := 1; j <= n; j++ { if matrix[i-1][j-1] == '1' { dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 maxSide = max(maxSide, dp[i][j]) } } } // 第二遍:收集所有最大正方形的右下角 var positions [][]int for i := 1; i <= m; i++ { for j := 1; j <= n; j++ { if dp[i][j] == maxSide { positions = append(positions, []int{i-1, j-1}) } } } return positions } ``` ## 总结 **核心要点**: 1. **动态规划**:dp[i][j] 表示以 (i, j) 为右下角的最大正方形边长 2. **状态转移**:取决于上、左、左上三个位置的最小值 3. **边界处理**:DP 数组多一圈,避免边界判断 **易错点**: - ❌ DP 数组大小错误(应该是 m+1 × n+1) - ❌ 索引偏移错误(dp[i][j] 对应 matrix[i-1][j-1]) - ❌ 忘记检查空矩阵 - ❌ min 函数实现错误 **为什么这样思考?** - 正方形的扩展需要三个方向都能支持 - 取最小值体现了"木桶效应" - DP 避免了暴力解法的重复计算 - 时间复杂度从 O(m² × n²) 优化到 O(m × n)