LeetCode 每日一题笔记
0. 前言
- 日期:2025.03.19
- 题目:3212.统计X和Y频数相等的子矩阵数量
- 难度:中等
- 标签:数组 二维前缀和 矩阵统计 哈希表
1. 题目理解
问题描述 :
给你一个二维字符矩阵 grid,其中 grid[i][j] 可能是 'X'、'Y' 或 '.',返回满足以下条件的子矩阵数量:
- 包含 grid[0][0](左上角元素);
- 子矩阵中 'X' 和 'Y' 的频数相等;
- 至少包含一个 'X'。
示例:
输入: grid = [["X","X"],["X","Y"]]
输出: 0
解释:
所有包含左上角的子矩阵:
(0,0)\]:X=1,Y=0 → 数量不等;
(0,0),(1,0)\]:X=2,Y=0 → 数量不等;
因此无满足条件的子矩阵,返回 0。
2. 解题思路
核心观察
- 题目要求子矩阵必须包含左上角,因此所有候选子矩阵都是以 (0,0) 为左上角、(i,j) 为右下角的矩形;
- 要满足 "X和Y数量相等",可将 X 记为 +1、Y 记为 -1、. 记为 0,此时子矩阵的总和为 0 等价于 X和Y数量相等;
- 需额外记录子矩阵是否包含至少一个 X(避免全是 Y 或 . 但总和为 0 的无效情况);
- 二维前缀和是高效计算子矩阵总和的核心方法,可避免重复计算。
算法步骤
-
构建差值前缀和数组:
- 定义
diff数组,diff[i][j]表示以 (0,0) 为左上角、(i,j) 为右下角的子矩阵中 X-Y 的差值(X=+1,Y=-1,.=0); - 利用二维前缀和公式计算
diff[i][j]:diff[i][j] = 左边值 + 上边值 - 左上值 + 当前字符的贡献值。
- 定义
-
构建含X标记数组:
- 定义
hasX数组,hasX[i][j]表示以 (0,0) 为左上角、(i,j) 为右下角的子矩阵是否包含至少一个 X; - 递推规则:当前位置或左边/上边的子矩阵包含 X,则
hasX[i][j] = true。
- 定义
-
统计符合条件的子矩阵:
- 遍历所有 (i,j),若
diff[i][j] == 0且hasX[i][j] == true,则计数 +1; - 最终返回计数结果。
- 遍历所有 (i,j),若
3. 代码实现
java
class Solution {
public int numberOfSubmatrices(char[][] grid) {
int count = 0;
int rows = grid.length; // 矩阵行数
int cols = grid[0].length; // 矩阵列数
// diff[i][j]:(0,0)到(i,j)子矩阵的X-Y差值(X=+1,Y=-1,.=0)
int[][] diff = new int[rows][cols];
// hasX[i][j]:(0,0)到(i,j)子矩阵是否包含至少一个X
boolean[][] hasX = new boolean[rows][cols];
// 第一步:计算差值前缀和数组diff,同时初始化hasX的基础值
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
char c = grid[i][j];
// 标记当前位置是否是X(用于后续hasX递推)
if (c == 'X') {
hasX[i][j] = true;
}
// 计算diff[i][j]的核心逻辑
int currentContribution = 0;
if (c == 'X') currentContribution = 1;
else if (c == 'Y') currentContribution = -1;
// '.' 贡献为0,无需处理
if (i == 0 && j == 0) {
// 左上角元素,直接赋值
diff[i][j] = currentContribution;
} else if (i == 0) {
// 第一行,仅累加左边的值
diff[i][j] = diff[i][j - 1] + currentContribution;
} else if (j == 0) {
// 第一列,仅累加上边的值
diff[i][j] = diff[i - 1][j] + currentContribution;
} else {
// 通用情况:左边 + 上边 - 左上(去重) + 当前贡献
diff[i][j] = diff[i][j - 1] + diff[i - 1][j] - diff[i - 1][j - 1] + currentContribution;
}
}
}
// 第二步:递推完善hasX数组(确保子矩阵包含至少一个X)
// 处理第一列(除左上角)
for (int i = 1; i < rows; i++) {
hasX[i][0] = hasX[i][0] || hasX[i - 1][0];
}
// 处理第一行(除左上角)
for (int j = 1; j < cols; j++) {
hasX[0][j] = hasX[0][j] || hasX[0][j - 1];
}
// 处理剩余位置
for (int i = 1; i < rows; i++) {
for (int j = 1; j < cols; j++) {
hasX[i][j] = hasX[i][j] || hasX[i - 1][j] || hasX[i][j - 1];
}
}
// 第三步:统计符合条件的子矩阵
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
// 条件:差值为0(X=Y)且包含至少一个X
if (diff[i][j] == 0 && hasX[i][j]) {
count++;
}
}
}
return count;
}
}
4. 代码优化说明
优化点1:合并hasX的计算逻辑(减少遍历次数)
原代码分多次遍历计算hasX,可将hasX的递推逻辑融合到diff的计算循环中,减少一次完整的矩阵遍历:
java
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
char c = grid[i][j];
int currentContribution = 0;
if (c == 'X') {
currentContribution = 1;
hasX[i][j] = true;
} else if (c == 'Y') {
currentContribution = -1;
}
// 计算diff(逻辑不变)
if (i == 0 && j == 0) {
diff[i][j] = currentContribution;
} else if (i == 0) {
diff[i][j] = diff[i][j - 1] + currentContribution;
hasX[i][j] |= hasX[i][j - 1]; // 融合hasX递推
} else if (j == 0) {
diff[i][j] = diff[i - 1][j] + currentContribution;
hasX[i][j] |= hasX[i - 1][j]; // 融合hasX递推
} else {
diff[i][j] = diff[i][j - 1] + diff[i - 1][j] - diff[i - 1][j - 1] + currentContribution;
hasX[i][j] |= hasX[i - 1][j] || hasX[i][j - 1]; // 融合hasX递推
}
}
}
优化点2:提前终止无效遍历(可选)
若某一行的diff值已经确定无法为0(如差值持续增大),可提前终止,但由于X/Y的随机性,该优化收益有限,仅适用于特定测试用例。
优化点3:空间优化(复用单个变量替代二维数组)
对于每行的计算,可仅保留上一行的diff值和hasX值,将空间复杂度从O(rows×cols)降至O(cols):
java
public int numberOfSubmatrices(char[][] grid) {
int count = 0;
int rows = grid.length;
int cols = grid[0].length;
int[] prevDiff = new int[cols]; // 上一行的diff值
boolean[] prevHasX = new boolean[cols]; // 上一行的hasX值
for (int i = 0; i < rows; i++) {
int[] currDiff = new int[cols];
boolean[] currHasX = new boolean[cols];
for (int j = 0; j < cols; j++) {
char c = grid[i][j];
int currentContribution = 0;
if (c == 'X') {
currentContribution = 1;
currHasX[j] = true;
} else if (c == 'Y') {
currentContribution = -1;
}
if (i == 0 && j == 0) {
currDiff[j] = currentContribution;
} else if (i == 0) {
currDiff[j] = currDiff[j - 1] + currentContribution;
currHasX[j] |= currHasX[j - 1];
} else if (j == 0) {
currDiff[j] = prevDiff[j] + currentContribution;
currHasX[j] |= prevHasX[j];
} else {
currDiff[j] = currDiff[j - 1] + prevDiff[j] - prevDiff[j - 1] + currentContribution;
currHasX[j] |= prevHasX[j] || currHasX[j - 1];
}
// 实时统计,无需后续遍历
if (currDiff[j] == 0 && currHasX[j]) {
count++;
}
}
prevDiff = currDiff;
prevHasX = currHasX;
}
return count;
}
5. 复杂度分析
-
时间复杂度:(O(rows \times cols))
- 核心逻辑仅需两次完整的矩阵遍历(计算diff/hasX + 统计结果),优化后可合并为一次遍历;
- 每个位置的计算都是常数时间 (O(1)),因此总时间复杂度为矩阵元素个数的线性级。
-
空间复杂度:
- 原版代码:(O(rows \times cols))(存储diff和hasX两个二维数组);
- 空间优化版:(O(cols))(仅保留上一行的diff和hasX值),大幅降低空间开销。
6. 总结
- 核心思路是二维前缀和 + 状态标记:将 X/Y 转换为数值差值,通过前缀和快速计算子矩阵的X-Y数量差,结合hasX标记筛选有效子矩阵;
- 关键技巧:把"X和Y数量相等"转化为"差值和为0"的数学问题,简化统计逻辑;
- 优化方向:融合遍历逻辑减少循环次数,或通过空间换时间的思路降低空间复杂度,可根据实际场景选择。