题目描述

✅ 1277. 统计全为 1 的正方形子矩阵

image-20260928224104677

image-20260928224104678

题意分析

统计矩阵中所有内部元素全部为一的正方形子矩阵。边必须与矩阵行列对齐,单个值为一的格子也算一个正方形,不同位置或不同边长都分别计数,允许重叠。

只要求边框为一并不够,内部也不能含零。返回的是正方形总数,不是最大边长、最大面积,也不是覆盖的不同格子数量。可以按每个正方形唯一的右下角把所有结果分组统计。

解法:右下角动态规划

核心思路

[!blue]

定义 dp[i][j] 为以当前格作为右下角的最大全一正方形边长。当前格为零时,任何以它为右下角的正方形都会含零,所以状态是零;当前格为一时,至少能组成边长一的正方形。

若要组成边长更大的正方形,上方、左方和左上方相邻格子处都必须能支持相应的小正方形。设这三个最大边长中的最小值为 k,三块边长为 k 的全一区域与当前这个一合起来,正好覆盖边长 k + 1 的正方形,因此可以达到这个大小。若再扩大一格,就要求三个邻居都至少支持 k + 1,与其中最小值为 k 矛盾。所以转移恰好是三个邻居最小值加一。

如果某个右下角的最大可行边长为 L,那么边长一到 L 的正方形都存在,且每种边长在这个右下角只有一个,对总数贡献恰好是 L。每个正方形又只有一个右下角,把所有格子的状态直接相加就不会重复或遗漏,无需单独枚举所有边长。

转移只依赖上一行和当前行左边,可以压缩成一维。处理当前格时,更新前的 dp[j] 是上方状态,已经更新的 dp[j - 1] 是左方状态,变量 leftUp 保存旧左上状态。先用 upper 保存将要覆盖的旧 dp[j],完成当前更新后,再令 leftUp = upper,交给下一列使用。

每行开始将 leftUp 置零,并保留额外的第零列为零,统一处理上边界和左边界。遇零格时必须把 dp[j] 清零,不能让上一行残留状态参与后续转移。

解题步骤

  1. 创建长度为列数加一的全零滚动数组,累计结果初始为零。
  2. 每行从左到右处理,行首将 leftUp 设为零。
  3. 更新当前格前,先保存旧上方值 upper = dp[j]。
  4. 当前元素为一时,令 dp[j] = min(上方, 左方, 左上方) + 1,并把新边长加入总数;为零时将 dp[j] 清零。
  5. 令 leftUp = upper,供下一列读取正确的旧左上状态。
  6. 所有格子处理完成后返回累加结果。

代码实现

class Solution {

    public int countSquares(int[][] matrix) {
        int m = matrix.length;
        int n = matrix[0].length;
        int[] dp = new int[n + 1];
        int res = 0;

        for (int i = 1; i <= m; i++) {
            int leftUp = 0;

            for (int j = 1; j <= n; j++) {
                // 保存上一行同列,稍后交给下一列作为旧左上角。
                int upper = dp[j];

                if (matrix[i - 1][j - 1] == 1) {
                    dp[j] = Math.min(Math.min(dp[j], dp[j - 1]), leftUp) + 1;
                    // 最大边长 L 同时贡献一到 L 的全部正方形。
                    res += dp[j];
                } else {
                    // 零格必须清掉旧行残留状态。
                    dp[j] = 0;
                }

                leftUp = upper;
            }
        }

        return res;
    }
}
func countSquares(matrix [][]int) int {

    m, n := len(matrix), len(matrix[0])
    dp := make([]int, n+1)
    res := 0

    for i := 1; i <= m; i++ {
        leftUp := 0
        for j := 1; j <= n; j++ {
            // 保存上一行同列,稍后交给下一列作为旧左上角。
            upper := dp[j]
            if matrix[i-1][j-1] == 1 {
                minVal := dp[j]
                if dp[j-1] < minVal {
                    minVal = dp[j-1]
                }
                if leftUp < minVal {
                    minVal = leftUp
                }
                dp[j] = minVal + 1
                // 最大边长 L 同时贡献一到 L 的全部正方形。
                res += dp[j]
            } else {
                // 零格必须清掉旧行残留状态。
                dp[j] = 0
            }

            leftUp = upper
        }
    }

    return res
}

复杂度分析

  • 时间复杂度:O(mn)。每个矩阵格子只处理一次,每次比较三个状态并累加。
  • 空间复杂度:O(n),只保留一行状态及少量变量,输入矩阵不被修改。

关键点总结

[!green]

  • 三个邻居最小值限制了可以共同扩张出的正方形,不能只看两个边长。
  • 最大边长 L 同时代表该右下角有 L 个正方形,所以求数量时累加状态。
  • 右下角是唯一归属,允许重叠也不会造成重复计数。
  • 一维数组中同时存在新行与旧行状态,旧对角必须在覆盖前单独保存。

易错点总结

[!yellow]

  • 取三个来源最大值:只要一个方向缺少支持,就无法扩张,必须由最短的一侧限制。
  • 漏掉左上状态:上方和左方的边长不能排除左上内部缺口,三者都需要检查。
  • 把状态平方后累加:状态的边长代表可选边长数量,平方得到面积,不是正方形个数。
  • 只维护全局最大状态:只能求最大正方形,不能得到所有正方形数量。
  • 遇零不清状态:会把上一行的可行边长错误带到当前零格。
  • 覆盖后再保存上方或不重置行首对角:下一格会读到错误的旧左上值,破坏滚动转移。

相似题目

题目 难度 关联与区别
221. 最大正方形 中等 局部DP相同,每格状态表示可行最大边长;本题将这些边长累加得到全部正方形数量。
1139. 最大的以 1 为边界的正方形 中等 原题只要求边框为1,本题内部也必须为1,不能只检查四条边。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2025/86850077
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!