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。具体为:
- 仅包含 (0,0):和为 7 ≤18
- 包含 (0,0)、(0,1):和为 7+6=13 ≤18
- 包含 (0,0)、(1,0):和为 7+6=13 ≤18
- 包含 (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](前缀和数组多开一行一列,避免边界判断)。
算法步骤
- 构建二维前缀和数组 :
- 定义
prefix数组,大小为(m+1) x (n+1)(m是行数,n是列数),初始值全为0; - 遍历原矩阵,按前缀和公式计算每个
prefix[i][j](对应原矩阵 (i-1,j-1) 为右下角的子矩阵和)。
- 定义
- 统计符合条件的子矩阵 :
- 遍历所有以 (0,0) 为左上角的子矩阵(即遍历
prefix数组的i≥1、j≥1位置); - 若当前
prefix[i][j] ≤ k,则计数+1; - 最终返回计数结果。
- 遍历所有以 (0,0) 为左上角的子矩阵(即遍历
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) 为左上角的子矩阵,无需枚举所有可能的子矩阵;
- 优化方向:非负矩阵可提前终止同一行的遍历,进一步减少计算量;空间敏感场景可直接修改原矩阵节省空间。