LeetCode 每日一题笔记 日期:2025.03.19 题目:3212.统计X和Y频数相等的子矩阵数量

LeetCode 每日一题笔记

0. 前言

  • 日期:2025.03.19
  • 题目:3212.统计X和Y频数相等的子矩阵数量
  • 难度:中等
  • 标签:数组 二维前缀和 矩阵统计 哈希表

1. 题目理解

问题描述

给你一个二维字符矩阵 grid,其中 grid[i][j] 可能是 'X'、'Y' 或 '.',返回满足以下条件的子矩阵数量:

  1. 包含 grid[0][0](左上角元素);
  2. 子矩阵中 'X' 和 'Y' 的频数相等;
  3. 至少包含一个 'X'。

示例

输入: grid = [["X","X"],["X","Y"]]

输出: 0

解释:

所有包含左上角的子矩阵:

  1. (0,0)\]:X=1,Y=0 → 数量不等;

  2. (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 的无效情况);
  • 二维前缀和是高效计算子矩阵总和的核心方法,可避免重复计算。

算法步骤

  1. 构建差值前缀和数组

    • 定义 diff 数组,diff[i][j] 表示以 (0,0) 为左上角、(i,j) 为右下角的子矩阵中 X-Y 的差值(X=+1,Y=-1,.=0);
    • 利用二维前缀和公式计算 diff[i][j]diff[i][j] = 左边值 + 上边值 - 左上值 + 当前字符的贡献值
  2. 构建含X标记数组

    • 定义 hasX 数组,hasX[i][j] 表示以 (0,0) 为左上角、(i,j) 为右下角的子矩阵是否包含至少一个 X;
    • 递推规则:当前位置或左边/上边的子矩阵包含 X,则 hasX[i][j] = true
  3. 统计符合条件的子矩阵

    • 遍历所有 (i,j),若 diff[i][j] == 0hasX[i][j] == true,则计数 +1;
    • 最终返回计数结果。

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"的数学问题,简化统计逻辑;
  • 优化方向:融合遍历逻辑减少循环次数,或通过空间换时间的思路降低空间复杂度,可根据实际场景选择。
相关推荐
Storynone2 小时前
【Day28】LeetCode:509. 斐波那契数,70. 爬楼梯,746. 使用最小花费爬楼梯
python·算法·leetcode
巧克力味的桃子3 小时前
国名排序题笔记(字符串函数 + fgets 详解)
笔记
四谎真好看3 小时前
Redis学习笔记(实战篇3)
redis·笔记·学习·学习笔记
bennybi3 小时前
Openclaw 实践笔记
笔记·ai·openclaw
AI视觉网奇3 小时前
aigc 生成几何图 整理笔记
笔记·aigc
博风4 小时前
算法:双指针解:盛最多水的容器
算法·leetcode
今儿敲了吗4 小时前
python基础学习笔记第五章——容器
笔记·python·学习
阿Y加油吧4 小时前
力扣打卡day05——找到字符串中所有字母异位词、和为K的子数组
leetcode
三水不滴4 小时前
Elasticsearch 实战系列(二):SpringBoot 集成 Elasticsearch,从 0 到 1 实现商品搜索系统
经验分享·spring boot·笔记·后端·elasticsearch·搜索引擎