每日一题 力扣 3548. 等和矩阵分割 II 前缀和 哈希表 C++ 题解

文章目录

题目描述

力扣 3548. 等和矩阵分割 II

示例 1:

输入: grid = [[1,4],[2,3]]

输出: true

解释:

在第 0 行和第 1 行之间进行水平分割,结果两部分的元素和为 1 + 4 = 5 和 2 + 3 = 5,相等。因此答案是 true。
示例 2:

输入: grid = [[1,2],[3,4]]

输出: true

解释:

在第 0 列和第 1 列之间进行垂直分割,结果两部分的元素和为 1 + 3 = 4 和 2 + 4 = 6。

通过从右侧部分移除 2 (6 - 2 = 4),两部分的元素和相等,并且两部分保持连通。因此答案是 true。
示例 3:

输入: grid = [[1,2,4],[2,3,5]]

输出: false

解释:

在第 0 行和第 1 行之间进行水平分割,结果两部分的元素和为 1 + 2 + 4 = 7 和 2 + 3 + 5 = 10。

通过从底部部分移除 3 (10 - 3 = 7),两部分的元素和相等,但底部部分不再连通(分裂为 [2] 和 [5])。因此答案是 false。
示例 4:

输入: grid = [[4,1,8],[3,2,6]]

输出: false

解释:

不存在有效的分割,因此答案是 false。
提示:

1 <= m == grid.length <= 105

1 <= n == grid[i].length <= 105

2 <= m * n <= 105

1 <= grid[i][j] <= 105

思路简述

有了昨日每日一题的铺垫(初次接触的朋友可以先参考我的上篇博客:每日一题 力扣 3546. 等和矩阵分割 I 前缀和 贪心 剪枝 C++ 题解),本题与前一题的核心区别在于:允许移除矩阵中最多一个元素,让分割线两侧的元素和相等,且移除元素后,分割对应的区域必须保持连通。

整体解题思路与前题相似,但本题放弃了数组形式的前缀和统计,改用变量累加的方式实现前缀和,大幅简化了空间开销,并引入了哈希表。

本题的核心难点是连通性判断 :删除元素后导致区域不连通的场景仅有两种 (我们视角是按照上半部分与左半部分进行分析不考虑下半部分与右半部分后面说明原因)------水平分割后上侧区域仅为一行垂直分割后左侧区域仅为一列 。这两种特殊场景下,我们只能删除所在行/列两端的元素 才能保证连通性,如下图所示:

如果单独为水平、垂直分割编写两套代码,再结合坐标特判、前缀和数组的坐标偏差,逻辑会非常繁琐。因此我们采用旋转复用 的技巧:仅实现一套水平切割 的判断逻辑,对矩阵执行4次顺时针90°旋转,即可复用同一套代码,完成水平、垂直所有方向的分割判定。

之后由于这道题可以删除一个元素,所以我们提前计算出这个x,我们设需要删除的元素值为x,分割线上半部分和为sum,矩阵总和为total,那么后面的这个灯饰我们一定能够认可 sum - x = total - sum,即可推导出核心公式:x = sum * 2 - total (公式有很多种不同的推导方式,选用该公式可以完全避免除法运算带来的精度误差)。

每次旋转后,我们先对矩阵做边界剪枝与特殊处理:

  • m < 2:由于矩阵不为空意味着当前矩阵仅有一行(m = 1),不满足水平切割的基本条件,直接旋转矩阵进入下一轮判断。
  • n == 1:当前矩阵仅有一列,属于一维数组的极端情况,虽然是极端情况课这个情况好处理,即当成一个一维数组单独遍历处理即可,只需要注意x坐标的合法性也就是连通性判断,这个判断就是上图所展示的情况,旋转矩阵进入下一轮。

完成剪枝后进入核心统计逻辑

最外层的循环逐行遍历矩阵,模拟枚举水平分割线。每确定一条分割线,并通过变量统计上半部分的元素和(变量实现前缀和),同时用哈希表记录上半部分的所有元素值。

同样提前计算x,针对计算结果分情况判断:

  • x = 0:无需删除任何元素,两部分和已相等,直接返回true
  • x是具体的数,这样我们知道了可以删除的值再去哈希表中进行查找,由于我们的矩阵旋转所以不用怕上半部分的和小于下半部分,应该是下半部分删除元素的问题,借助矩阵旋转,我们无需处理下半部分删除元素的场景------旋转后下半部分会变为上半部分,可被同一套逻辑覆盖,找到目标值x后,结合连通性规则校验:若上半部分仅一行,仅校验行两端元素是否等于x,若上半部分≥2行,任意元素均可删除,直接校验哈希表即可。

注:这里哈希只需要统计上半部分x是否出现过即可,因为特殊位置是固定的所以我们无需统计x的位置

最后对矩阵执行顺时针90°旋转,进入下一轮方向的判断。

矩阵旋转实现

通过公式 tmp[j][m-1-i] = grid[i][j] 实现矩阵顺时针90°旋转,核心作用是将原矩阵的列转换为行,让垂直分割等价为水平分割,实现代码复用。

代码实现

cpp 复制代码
class Solution {
public:
    // 矩阵顺时针旋转90度
    // 核心作用:将 列 → 转换为行,让垂直分割可以复用水平分割的代码
    vector<vector<int>> rotation(vector<vector<int>>& grid) {
        int m = grid.size(), n = grid[0].size();
        // 旋转后矩阵尺寸:n行 m列
        vector<vector<int>> tmp(n, vector<int>(m,0)); 
        
        for (int i = 0; i < m; i++) 
            for (int j = 0; j < n; j++) 
                // 坐标变换公式:原(i,j) → 旋转后(j, m-1-i)
                tmp[j][m - 1 - i] = grid[i][j];
                
        return tmp;
    }

    bool canPartitionGrid(vector<vector<int>>& grid) {
        long long total = 0;  // 矩阵所有元素的总和
        long long sum;        // 分割线上半部分的元素和(前缀和)
        long long x;          // 需要删除的元素值:sum - x = total - sum → x = 2*sum - total
        int m = grid.size();  // 矩阵行数
        int n = grid[0].size();// 矩阵列数
        unordered_set<long long> exist; // 存储分割线上半部分的所有元素值(不存下标)

        // 第一步:计算整个矩阵的元素总和
        for (int i = 0; i < m; i++) 
            for (int j = 0; j < n; j++) 
                total += grid[i][j];

        // 旋转4次:覆盖 水平/垂直 四个分割方向(上→下、下→上、左→右、右→左)
        for (int k = 0; k < 4; k++) 
        {
            exist.clear();       // 清空哈希表,重新记录当前分割的上半部分元素
            exist.insert(0);     // 初始化占位(无实际作用,兼容边界)
            sum = 0;             // 重置上半部分和
            m = grid.size();     // 更新当前旋转后的矩阵行数
            n = grid[0].size();  // 更新当前旋转后的矩阵列数

            // 剪枝1:当前矩阵只有1行,无法水平切割,直接旋转下一个方向
            if (m < 2) 
            {
                grid = rotation(grid);
                continue;// 直接进入下一轮
            }

            // 剪枝2:特殊情况 → 矩阵只有1列(一维数组),单独处理
            if(n == 1)
            {
                for(int i = 0; i < m - 1; i++)
                {
                    sum += grid[i][0];
                    x = sum * 2 - total;
                    // 满足条件:无需删除 或 删除列的两端元素(保证连通)
                    if(x == 0 || x == grid[0][0] || x == grid[i][0])
                    {
                        return true;
                    }
                }
                grid = rotation(grid);
                continue;// 直接进入下一轮
            }

            // 核心逻辑:枚举所有水平分割线(遍历到倒数第二行,保证下半部分非空)
            for (int i = 0; i < m - 1; i++) 
            {
                // 累加当前行所有元素,存入哈希表(哈希表只保留分割线上半部分)
                for(int j = 0; j < n; j++)
                {
                    exist.insert(grid[i][j]);
                    sum += grid[i][j];
                }

                // 计算需要删除的元素值
                x = sum * 2 - total;

                // 情况1:分割后上半部分只有 1 行(i=0)
                // 连通性规则:只能删除行的 左右两端 元素
                if(i == 0)
                {
                    if(x == 0 || x == grid[0][0] || x == grid[0][n - 1])
                        return true;
                
                    continue;// 直接进入下一轮
                }

                // 情况2:分割后上半部分 ≥ 2 行
                // 连通性规则:删除任意元素都连通,只要哈希表存在x就合法
                if(exist.contains(x))
                    return true;
            }

            // 当前方向检查完毕,旋转矩阵,检查下一个方向
            grid = rotation(grid);
        }

        // 四个方向都检查完毕,无有效分割
        return false;
    }
};

复杂度分析

  • 时间复杂度 : O ( m n ) O(mn) O(mn)
    矩阵共旋转4次,每次旋转与遍历的时间复杂度均为矩阵元素总个数 m n mn mn,常数系数不影响复杂度量级。
  • 空间复杂度 : O ( m n ) O(mn) O(mn)
    主要开销为旋转矩阵的临时数组、哈希表存储的元素值,均与矩阵元素个数线性相关。

踩坑记录

  1. 最初尝试使用二维数组统计前缀和,但后续被连通性的判定条件绕晕,甚至混淆了矩阵的行和列,导致逻辑混乱。
  2. 一开始想用unordered_map建立元素值与下标的映射,来校验连通性,看完题解后发现了更简洁的思路:仅统计分割线一侧的元素,结合固定坐标校验特殊位置即可,完全不需要记录下标,大幅简化了代码逻辑。

如果这篇博客对你有帮助,别忘了点赞支持一下~也可以收藏起来,方便后续刷题复习时随时翻看。要是能顺手点个关注,爱弥斯还能得到漂泊者批准的游戏时间哦!

相关推荐
进击的小头2 小时前
第19篇:卡尔曼滤波器与MPC模型预测控制器的结合实战
python·算法
2501_908329852 小时前
C++中的装饰器模式
开发语言·c++·算法
2301_788770552 小时前
OJ模拟2
数据结构·算法
木井巳2 小时前
【递归算法】全排列 Ⅱ
java·算法·leetcode·决策树·深度优先·剪枝
岑梓铭2 小时前
《考研408数据结构》第三章3(数组矩阵)复习笔记
数据结构·笔记·矩阵
Fcy6482 小时前
算法竞赛有关数据结构的补充(3)—— 二叉树、堆和哈希表的静态实现(包括红黑树和AVL树动态实现)
数据结构·算法·散列表
代码探秘者2 小时前
【算法篇】6.分治
java·数据结构·后端·python·算法·排序算法
TechPioneer_lp2 小时前
2026微软SDE LeetCode高频题:208道,按频度排序,含备考建议
算法·leetcode·microsoft·leetcode刷题·大厂算法刷题·微软sde·微软笔试题
科德航空的张先生2 小时前
空管模拟器在塔台指挥训练中的应用与效能分析
人工智能·算法