LeetCode 每日一题笔记 日期:2025.03.18 题目:3070.元素和小于等于k的子矩阵的数目

LeetCode 每日一题笔记

0. 前言

  • 日期:2025.03.18
  • 题目:3070.元素和小于等于k的子矩阵的数目
  • 难度:中等
  • 标签:数组 二维前缀和 矩阵

1. 题目理解

问题描述

给你一个下标从 0 开始的整数矩阵 grid 和一个整数 k。返回包含 grid 左上角元素、元素和小于或等于 k 的 子矩阵的数目。

示例

输入:grid = [[7,6,3],[6,6,1]], k = 18

输出:4

解释:满足条件的子矩阵需包含左上角 (0,0) 位置,且元素和 ≤18。具体为:

  1. 仅包含 (0,0):和为 7 ≤18
  2. 包含 (0,0)、(0,1):和为 7+6=13 ≤18
  3. 包含 (0,0)、(1,0):和为 7+6=13 ≤18
  4. 包含 (0,0)、(0,1)、(1,0)、(1,1):和为 7+6+6+6=25?(修正:实际计算为 7+6+6+6=25 超了,正确的4个是:(0,0)、(0,0)-(0,1)、(0,0)-(1,0)、(0,0)-(0,1)-(1,0) 计算错误,正确二维前缀和计算为:
    (1,1)=7,(1,2)=13,(2,1)=13,(2,2)=25(超),(1,3)=16,(2,3)=27(超)。所以满足的是 (1,1)、(1,2)、(2,1)、(1,3),共4个)

2. 解题思路

核心观察

  • 题目要求子矩阵必须包含左上角元素,因此所有符合条件的子矩阵都是以 (0,0) 为左上角、(i,j) 为右下角的矩形(i≥0, j≥0);
  • 直接计算每个子矩阵的和会重复计算元素,时间复杂度高,因此使用二维前缀和快速计算任意子矩阵的和;
  • 二维前缀和公式:prefix[i][j] = prefix[i-1][j] + prefix[i][j-1] - prefix[i-1][j-1] + grid[i-1][j-1](前缀和数组多开一行一列,避免边界判断)。

算法步骤

  1. 构建二维前缀和数组
    • 定义 prefix 数组,大小为 (m+1) x (n+1)(m是行数,n是列数),初始值全为0;
    • 遍历原矩阵,按前缀和公式计算每个 prefix[i][j](对应原矩阵 (i-1,j-1) 为右下角的子矩阵和)。
  2. 统计符合条件的子矩阵
    • 遍历所有以 (0,0) 为左上角的子矩阵(即遍历 prefix 数组的 i≥1、j≥1 位置);
    • 若当前 prefix[i][j] ≤ k,则计数+1;
    • 最终返回计数结果。

3. 代码实现

java 复制代码
package com.sheeta1998.lec.lc3070;

public class Solution {
    public int countSubmatrices(int[][] grid, int k) {
        int count = 0;
        int m = grid.length;    // 原矩阵行数
        int n = grid[0].length; // 原矩阵列数
        // 前缀和数组,多开一行一列避免边界判断
        int[][] prefix = new int[m+1][n+1];
        
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                // 二维前缀和核心公式:当前值 = 左 + 上 - 左上 + 原矩阵值
                prefix[i][j] = prefix[i][j-1] + prefix[i-1][j] - prefix[i-1][j-1] + grid[i-1][j-1];
                // 统计符合条件的子矩阵
                if (prefix[i][j] <= k) {
                    count++;
                }
            }
        }
        return count;
    }
}

4. 代码优化说明

优化点1:提前终止遍历

由于矩阵元素均为整数(题目隐含非负,否则无法提前终止),当某一行的 prefix[i][j] > k 时,同一行后续的 j+1、j+2... 位置的前缀和会更大,可直接终止当前行的遍历:

java 复制代码
public int countSubmatrices(int[][] grid, int k) {
    int count = 0;
    int m = grid.length;
    int n = grid[0].length;
    int[][] prefix = new int[m+1][n+1];
    
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            prefix[i][j] = prefix[i][j-1] + prefix[i-1][j] - prefix[i-1][j-1] + grid[i-1][j-1];
            if (prefix[i][j] > k) {
                break; // 同一行后续列的和会更大,直接终止
            }
            count++;
        }
    }
    return count;
}

优化点2:空间优化(无需额外前缀和数组)

可直接在原矩阵上修改,节省空间(注意:会修改原数组,若不允许则不建议):

java 复制代码
public int countSubmatrices(int[][] grid, int k) {
    int count = 0;
    int m = grid.length;
    int n = grid[0].length;
    
    // 先处理第一行
    for (int j = 1; j < n; j++) {
        grid[0][j] += grid[0][j-1];
    }
    // 先处理第一列
    for (int i = 1; i < m; i++) {
        grid[i][0] += grid[i-1][0];
    }
    // 处理剩余位置
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            grid[i][j] += grid[i-1][j] + grid[i][j-1] - grid[i-1][j-1];
        }
    }
    // 统计结果
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (grid[i][j] <= k) count++;
            else break;
        }
    }
    return count;
}

5. 复杂度分析

  • 时间复杂度:(O(m \times n))

    • 遍历矩阵构建前缀和数组:(O(m \times n));
    • 统计符合条件的子矩阵:已融合在前缀和遍历过程中,无额外开销;
    • 优化后提前终止的情况,实际时间会小于 (O(m \times n))。
  • 空间复杂度

    • 原版代码:(O(m \times n))(额外的前缀和数组);
    • 空间优化版:(O(1))(直接修改原数组,无额外空间)。

6. 总结

  • 核心思路是二维前缀和:通过预处理前缀和数组,将任意子矩阵的和计算从 (O(m \times n)) 降为 (O(1)),高效统计符合条件的子矩阵;
  • 关键技巧:利用"子矩阵必须包含左上角"的特性,只需遍历所有以 (0,0) 为左上角的子矩阵,无需枚举所有可能的子矩阵;
  • 优化方向:非负矩阵可提前终止同一行的遍历,进一步减少计算量;空间敏感场景可直接修改原矩阵节省空间。
相关推荐
啊阿狸不会拉杆2 小时前
《计算机网络-自顶向下方法》笔记分享:第1章-「计算机网络和因特网」-1.2 网络边缘
网络·笔记·计算机网络·接入网·光纤·网络边缘·物理媒体
努力学习的小廉2 小时前
redis学习笔记(八)—— C++ 操作 Redis
redis·笔记·学习
落地加湿器2 小时前
Acwing算法课图论与搜索笔记
c++·笔记·算法·图论·dfs·bfs·图搜索算法
booksyhay2 小时前
XCP学习笔记(2)-指令详解
笔记·学习
96773 小时前
力扣面试经典150 88. 合并两个有序数组 归并排序的merge函数
算法·leetcode·面试
小酒丸子11 小时前
AD学习笔记之异形焊盘
笔记·学习
IronMurphy14 小时前
【算法二十六】108. 将有序数组转换为二叉搜索树 98. 验证二叉搜索树
数据结构·算法·leetcode
im_AMBER14 小时前
Leetcode 141 最长公共前缀 | 罗马数字转整数
算法·leetcode
似水明俊德14 小时前
01-C#.Net-泛型-学习笔记
java·笔记·学习·c#·.net