【LeetCode 每日一题】1277. 统计全为 1 的正方形子矩阵

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

文章目录

整体思路

这段代码旨在解决一个经典的二维矩阵问题:统计全为 1 的正方形子矩阵个数 (Count Square Submatrices with All Ones)。问题要求计算在一个由 0 和 1 组成的矩阵中,所有元素都为 1 的正方形子矩阵的总数量。

该算法采用了一种非常高效的 动态规划 (Dynamic Programming) 方法。其核心思想是构建一个 DP 表,并利用子问题的解来推导当前问题的解。

  1. 状态定义 (DP State)

    • 算法创建了一个 dp 数组,其大小比原矩阵 matrix 大一圈([m+1][n+1]),这种"填充"技巧可以简化边界条件的处理。
    • dp[i][j] 的核心含义是:matrix[i-1][j-1] 为右下角 的、全由 1 组成的最大正方形的 边长
  2. 状态转移方程

    • 算法遍历原矩阵 matrix 的每一个单元格 (i, j)
    • 只有当 matrix[i][j] 本身为 1 时,它才有可能成为某个正方形的右下角。
    • 如果 matrix[i][j] == 1,那么以它为右下角的最大正方形的边长,取决于其 上方左方左上方 三个相邻位置所能形成的最大正方形。
    • 具体来说,一个新的、更大的 k x k 正方形,必须依赖于它旁边已经存在三个 (k-1) x (k-1) 的正方形。因此,新的边长受限于这三者中的最小值。
    • 状态转移方程为:
      dp[i+1][j+1] = min(dp[i][j+1], dp[i+1][j], dp[i][j]) + 1
      (这里的 dp 索引都比 matrix 的索引大 1)。
  3. 核心计数逻辑

    • 这是该算法最巧妙的一点。dp[i+1][j+1] 的值,比如说 k,不仅代表以 matrix[i][j] 为右下角的最大正方形边长是 k,它还隐含 了以该点为右下角的、边长为 k-1, k-2, ..., 1 的正方形也必然存在。
    • 因此,dp[i+1][j+1] 的值 k,恰好等于matrix[i][j] 为右下角的所有正方形的总数
    • 所以,在计算出每个 dp[i+1][j+1] 的值之后,直接将其累加到最终结果 ans 中,就可以统计出矩阵中所有的正方形。
  4. 算法流程

    • 创建一个 dp 表。
    • 遍历输入矩阵 matrix
    • 如果 matrix[i][j] 是 1,则根据状态转移方程计算 dp[i+1][j+1] 的值。
    • 将计算出的 dp[i+1][j+1] 累加到 ans
    • 如果 matrix[i][j] 是 0,则 dp[i+1][j+1] 保持默认值 0,对 ans 没有贡献,这符合逻辑。
    • 遍历结束后返回 ans

完整代码

java 复制代码
class Solution {
    /**
     * 统计一个二维矩阵中,所有元素都为 1 的正方形子矩阵的总数量。
     * @param matrix 一个由 0 和 1 组成的二维整数数组
     * @return 全为 1 的正方形子矩阵的总数
     */
    public int countSquares(int[][] matrix) {
        int m = matrix.length;
        int n = matrix[0].length;
        
        // dp 表:dp[i][j] 表示以 matrix[i-1][j-1] 为右下角的最大正方形的边长。
        // 使用 m+1 和 n+1 的大小是为了增加一圈"哨兵"0,简化边界条件的处理。
        int[][] dp = new int[m + 1][n + 1];
        
        // ans: 用于累计正方形的总数。
        int ans = 0;
        
        // 遍历原始矩阵的每一个单元格
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                // 只有当当前单元格为 1 时,它才可能成为正方形的一部分
                if (matrix[i][j] == 1) {
                    // 状态转移方程:
                    // 以 (i, j) 为右下角的最大正方形的边长,取决于其左、上、左上三个方向
                    // 形成的最大正方形边长中的最小值,然后再加上当前这个单元格(+1)。
                    // dp[i+1][j+1] 对应 matrix[i][j]。
                    dp[i + 1][j + 1] = Math.min(Math.min(dp[i][j + 1], dp[i + 1][j]), dp[i][j]) + 1;
                    
                    // 核心计数逻辑:
                    // dp[i+1][j+1] 的值 k,代表以 (i, j) 为右下角的正方形有 k 个
                    // (边长分别为 1, 2, ..., k)。
                    // 因此,直接将这个值累加到总数中。
                    ans += dp[i + 1][j + 1];
                }
            }
        }
        
        // 返回最终统计的结果
        return ans;
    }
}

时空复杂度

时间复杂度:O(m * n)

  1. 循环 :算法的核心是两个嵌套的 for 循环,它们完整地遍历了输入矩阵 matrix 的每一个单元格。
    • 外层循环执行 m 次(m 是矩阵的行数)。
    • 内层循环执行 n 次(n 是矩阵的列数)。
  2. 循环内部操作
    • 在循环的每一次迭代中,执行的操作(if 判断、Math.min、数组读写、加法)都是常数时间复杂度的,即 O(1)

综合分析

算法的总时间复杂度是 m * n 次 O(1) 操作,因此最终的时间复杂度为 O(m * n)

空间复杂度:O(m * n)

  1. 主要存储开销 :算法创建了一个名为 dp 的二维数组来存储动态规划的状态。
  2. 空间大小dp 数组的维度是 (m + 1) x (n + 1)。其占用的空间与输入矩阵的大小 m * n 成正比。
  3. 其他变量m, n, ans, i, j 等变量都只占用常数级别的空间。

综合分析

算法所需的额外辅助空间主要由 dp 数组决定。因此,其空间复杂度为 O(m * n)

参考灵神

相关推荐
古译汉书4 小时前
嵌入式铁头山羊stm32-ADC实现定时器触发的注入序列的单通道转换-Day26
开发语言·数据结构·stm32·单片机·嵌入式硬件·算法
野犬寒鸦4 小时前
力扣hot100:相交链表与反转链表详细思路讲解(160,206)
java·数据结构·后端·算法·leetcode
阿昭L4 小时前
leetcode两数之和
算法·leetcode
周树皮不皮4 小时前
【Leetcode100】算法模板之二叉树
算法
无名客04 小时前
sentinel限流常见的几种算法以及优缺点
算法·sentinel·限流
Lris-KK4 小时前
【Leetcode】高频SQL基础题--1164.指定日期的产品价格
sql·leetcode
Moonbit5 小时前
月报Vol.03: 新增Bitstring pattern支持,构造器模式匹配增强
后端·算法·github
快手技术5 小时前
多模态大模型Keye-VL-1.5发布!视频理解能力更强!
算法
薛定谔的算法5 小时前
JavaScript数组操作完全指南:从基础到高级
前端·javascript·算法