文章目录

题目描述
示例 1:
输入: grid = [[1,4],[2,3]]
输出: true
解释:
在第 0 行和第 1 行之间进行水平分割,得到两个非空部分,每部分的元素之和为 5。因此,答案是 true。
示例 2:输入: grid = [[1,3],[2,4]]
输出: false
解释:
无论是水平分割还是垂直分割,都无法使两个非空部分的元素之和相等。因此,答案是 false。
提示:1 <= m == grid.length <= 105
1 <= n == grid[i].length <= 105
2 <= m * n <= 105
1 <= grid[i][j] <= 105
方法一:前缀和
思路简述
我们可以使用二维数组 sums[i][j] 来统计以 (i-1, j-1) 为右下角、(0, 0) 为左上角的矩形区域的元素和。
前缀和的计算有两种常见写法:
- 写法一:
sums[i+1][j+1] = sums[i][j+1] + sums[i+1][j] - sums[i][j] + grid[i][j](对应sums[i][j]表示原矩阵前i行、前j列的和); - 写法二:
sums[i][j] = sums[i][j-1] + sums[i-1][j] - sums[i-1][j-1] + grid[i][j](对应sums[i][j]表示以(i,j)为右下角的矩形和)。
两种写法本质相同,仅下标偏移不同,选择一种即可
关于二维前缀和如何计算任意矩形区域的和,我在之前的博客中已配合图片详细讲解,这里不再赘述,感兴趣的朋友可以参考:C++ 前缀和 高频笔试考点 实用技巧 牛客 DP35 二维模板 题解 每日一题。
得到前缀和数组后,整个矩阵的总和即为 sums[m][n](也可单独统计变量 sum,代码更直观)。我们只需检查是否存在某一行 i,使得 sums[i][n] * 2 == sum(水平分割,蓝色部分),或某一列 j,使得 sums[m][j] * 2 == sum(垂直分割,绿色部分)。如下图所示,边界处的前缀和若满足两倍关系,则对应位置存在合法分割线:

注:图片中的颜色并不是只得具体矩形而是区域,具体每个点代表的矩形太多画出来太乱所以就没画
代码实现
cpp
class Solution {
public:
bool canPartitionGrid(vector<vector<int>>& grid)
{
// 获取矩阵的行数 m 和列数 n
int m = grid.size(), n = grid[0].size();
// 定义前缀和数组 sums,大小为 (m+1) x (n+1),使用 1-based 索引避免边界越界
// sums[i][j] 表示原矩阵中前 i 行、前 j 列组成的矩形区域的元素和
vector<vector<long long>> sums(m+1, vector<long long>(n+1, 0));
long long sum = 0; // 用于统计整个矩阵的元素总和
// 遍历原矩阵,计算前缀和数组并累加总和
for(int i = 0; i < m; i++)
{
for(int j = 0; j < n; j++)
{
// 前缀和核心公式:
// sums[i+1][j+1] = 上方矩形和 + 左侧矩形和 - 重复计算的左上角矩形和 + 当前元素
sums[i+1][j+1] = sums[i][j+1] + sums[i+1][j] - sums[i][j] + grid[i][j];
sum += grid[i][j]; // 累加当前元素到总和
}
}
// 检查是否存在水平分割线:遍历每一行 i
// 若前 i 行的和 sums[i][n] 的两倍等于总和,说明可以水平分割
for(int i = 0; i <= m; i++)
{
if(sum == sums[i][n] * 2)
return true;
}
// 检查是否存在垂直分割线:遍历每一列 j
// 若前 j 列的和 sums[m][j] 的两倍等于总和,说明可以垂直分割
for(int i = 0; i <= n; i++)
{
if(sum == sums[m][i] * 2)
return true;
}
// 若水平和垂直分割都不满足,返回 false
return false;
}
};
复杂度分析
- 时间复杂度 : O ( m n ) O(mn) O(mn)。需遍历一次矩阵预处理前缀和,再分别遍历行和列检查分割条件,总操作数与矩阵元素数量成正比。
- 空间复杂度 : O ( m n ) O(mn) O(mn)。需创建一个 ( m + 1 ) × ( n + 1 ) (m+1) \times (n+1) (m+1)×(n+1) 的前缀和数组。
方法二:暴力+贪心+剪枝
思路简述
为了避免前缀和数组的空间开销,我们可以结合剪枝 与贪心思想,仅通过变量累加完成判断,实际运行速度通常比方法一更优。
- 剪枝逻辑 :若矩阵总和为奇数,则必然无法分割为两个和相等的整数部分,直接返回
false。(总和除二为小数,但是题目中数组中的数字必然为整数,逻辑上不成立) - 贪心策略 :分别尝试水平和垂直分割:
- 水平分割时,逐行累加元素和,若累加和等于总和的一半,则找到合法分割;若超过一半,由于元素均为正整数,后续行累加只会更大,可提前终止(剪枝)。
- 垂直分割同理,逐列累加检查即可。
该方法的缺点是通用性较弱,仅适用于本题场景,但在时间和空间上更具优势。
代码实现
cpp
class Solution {
public:
using ll = long long; // 为 long long 定义别名 ll,简化代码书写
bool canPartitionGrid(vector<vector<int>>& grid) {
const int rows = grid.size(); // 获取矩阵行数(用 const 防止意外修改)
const int cols = grid[0].size(); // 获取矩阵列数
// 第一步:计算整个矩阵的元素总和
ll total = 0;
// 范围 for 循环遍历每一行
for (auto& row : grid)
{
for (int num : row)
total += num; // 累加当前行的所有元素
}
// 关键剪枝:若总和为奇数,必然无法分割为两个和相等的整数部分,直接返回 false
if (total % 2 != 0)
return false;
ll target = total / 2; // 目标值:分割后每部分的和需要等于总和的一半
// 第二步:检查是否存在水平分割线(逐行累加)
ll cur = 0; // 记录当前累加和
for (auto& row : grid)
{
// 累加当前行的所有元素
for (int num : row)
cur += num;
// 若当前累加和等于目标值,找到合法分割
if (cur == target)
return true;
// 贪心剪枝:元素均为正整数,若超过目标值,后续只会更大,直接终止
if (cur > target)
break;
}
// 第三步:检查是否存在垂直分割线(逐列累加)
cur = 0; // 重置当前累加和
// 外层循环遍历列
for (int j = 0; j < cols; ++j)
{
// 内层循环遍历行,累加当前列的所有元素
for (int i = 0; i < rows; ++i)
cur += grid[i][j];
// 若当前累加和等于目标值,找到合法分割
if (cur == target)
return true;
// 同理,超过目标值则提前终止
if (cur > target)
break;
}
// 若水平和垂直分割都不满足,返回 false
return false;
}
};
复杂度分析
- 时间复杂度 : O ( m n ) O(mn) O(mn)。最坏情况下需遍历整个矩阵,但由于剪枝的存在,实际运行通常更快。
- 空间复杂度 : O ( 1 ) O(1) O(1)。仅使用常数个变量存储总和、目标值和当前累加和。
踩坑记录
- 边界处理:注意前缀和数组的下标偏移需与定义一致,避免越界或计算错误;
- 映射关系 :理清前缀和数组与原矩阵的对应关系,切勿混淆
sums[i][j]表示的区域范围; - 溢出风险 :矩阵元素和可能较大,需使用
long long类型存储前缀和与总和,防止整数溢出。
如果这篇博客对你有帮助,别忘了点赞支持一下~也可以收藏起来,方便后续刷题复习时随时翻看。要是能顺手点个关注,爱弥斯还能得到漂泊者批准的游戏时间哦!


