☰
LeetCode 3546题解:等和矩阵分割算法与优化
2026/11/5 2:22:31 网站建设 项目流程

1. 问题重述与理解

LeetCode 3546题"等和矩阵分割I"要求我们判断一个给定的m×n正整数矩阵grid是否可以通过一条水平或垂直分割线将其分成两个非空部分,且这两部分的元素之和相等。

关键约束条件:

  • 只能使用一条水平或垂直分割线
  • 分割后的两部分都必须是非空的
  • 两部分元素之和必须完全相等

2. 解题思路分析

2.1 基础数学原理

首先,我们需要理解这个问题的数学本质。要使矩阵能够被分割成两个和相等的部分,整个矩阵的总和必须是一个偶数。因为:

总和 = 部分1和 + 部分2和 且 部分1和 = 部分2和 所以 总和 = 2 × 部分1和 → 总和必须是偶数

因此,我们的算法可以首先计算整个矩阵的元素总和,如果总和是奇数,直接返回false。

2.2 分割线位置的可能性

对于m×n的矩阵,有m-1种可能的水平分割线和n-1种可能的垂直分割线:

  • 水平分割线:在第i行和第i+1行之间分割(i从0到m-2)
  • 垂直分割线:在第j列和第j+1列之间分割(j从0到n-2)

我们需要检查所有这些可能的分割线位置,看看是否存在一个位置使得分割后的两部分和相等。

2.3 遍历策略优化

直接检查所有可能的分割线位置虽然可行,但效率不高。我们可以采用更聪明的方法:

  1. 首先计算整个矩阵的总和sum
  2. 如果sum是奇数,直接返回false
  3. 否则,计算目标值target = sum / 2
  4. 然后:
    • 按行累加,检查是否在某行后达到target
    • 按列累加,检查是否在某列后达到target

这种方法只需要三次遍历:

  • 第一次:计算总和
  • 第二次:按行检查水平分割
  • 第三次:按列检查垂直分割

3. 详细算法实现

3.1 总和计算

首先遍历整个矩阵,计算所有元素的总和:

long long sum = 0; for (auto& row : grid) { for (int num : row) { sum += num; } }

这里使用long long类型是为了防止大数溢出。

3.2 奇偶检查

如果总和是奇数,直接返回false:

if (sum % 2 != 0) { return false; } long long target = sum / 2;

3.3 水平分割检查

从上到下逐行累加,检查是否在某行后达到target:

long long current = 0; for (auto& row : grid) { for (int num : row) { current += num; } if (current == target) { return true; } if (current > target) { break; // 已经超过,后续只会更大 } }

3.4 垂直分割检查

从左到右逐列累加,检查是否在某列后达到target:

current = 0; for (int j = 0; j < grid[0].size(); ++j) { for (int i = 0; i < grid.size(); ++i) { current += grid[i][j]; } if (current == target) { return true; } if (current > target) { break; // 已经超过,后续只会更大 } }

3.5 完整代码实现

将上述部分组合起来,得到完整解法:

class Solution { public: bool canPartitionGrid(vector<vector<int>>& grid) { long long sum = 0; for (auto& row : grid) { for (int num : row) { sum += num; } } if (sum % 2 != 0) { return false; } long long target = sum / 2; // 检查水平分割 long long current = 0; for (auto& row : grid) { for (int num : row) { current += num; } if (current == target) { return true; } if (current > target) { break; } } // 检查垂直分割 current = 0; for (int j = 0; j < grid[0].size(); ++j) { for (int i = 0; i < grid.size(); ++i) { current += grid[i][j]; } if (current == target) { return true; } if (current > target) { break; } } return false; } };

4. 复杂度分析

4.1 时间复杂度

  • 总和计算:需要遍历整个矩阵,O(m×n)
  • 水平分割检查:最坏情况下需要遍历整个矩阵,O(m×n)
  • 垂直分割检查:最坏情况下需要遍历整个矩阵,O(m×n)

因此,总时间复杂度为O(m×n)。由于三次遍历是顺序进行的,常数因子为3,可以忽略。

4.2 空间复杂度

算法只使用了常数个额外变量(sum, target, current),因此空间复杂度为O(1)。

5. 边界条件与测试用例

5.1 典型测试用例

  1. 示例1:

    [[1,4],[2,3]]

    预期输出:true(水平分割在第0行和第1行之间)

  2. 示例2:

    [[1,3],[2,4]]

    预期输出:false

  3. 单行矩阵:

    [[1,2,3,4,10]]

    预期输出:true(垂直分割在第3列和第4列之间)

  4. 单列矩阵:

    [[1],[2],[3],[4]]

    预期输出:true(水平分割在第1行和第2行之间)

  5. 总和为奇数:

    [[1,2],[3,4]]

    预期输出:false(总和10,无法均分)

5.2 特殊边界情况

  1. 最小矩阵2×1:

    [[1],[1]]

    预期输出:true

  2. 最小矩阵1×2:

    [[1,1]]

    预期输出:true

  3. 所有元素相同:

    [[2,2],[2,2]]

    预期输出:true(多种分割方式)

6. 优化与扩展思考

6.1 可能的优化

  1. 提前终止:一旦在某次分割检查中找到解,可以立即返回,不需要继续检查其他可能性。

  2. 并行计算:对于非常大的矩阵,可以并行计算行和和列和,但会增加实现复杂度。

  3. 预处理前缀和:对于需要多次查询的情况,可以预处理行和列的前缀和数组。

6.2 问题扩展

  1. 如果允许任意形状的分割,而不仅限于直线分割,问题将变得更加复杂,可能需要使用动态规划或其他高级算法。

  2. 如果要求找出所有可能的分割方式,而不仅仅是判断是否存在,我们需要记录所有满足条件的分割线位置。

  3. 如果矩阵包含负数,当前算法仍然适用,但需要注意累加过程中可能出现的和减少情况,不能提前终止。

7. 实际应用场景

这种矩阵分割问题在实际中有多种应用:

  1. 资源分配:将资源矩阵划分为两个价值相等的部分
  2. 图像处理:将图像分成两个区域进行并行处理
  3. 负载均衡:将任务矩阵均衡分配到两个处理单元
  4. 数据分区:在分布式系统中均衡分配数据

8. 常见错误与注意事项

  1. 整数溢出:对于大矩阵,元素和可能超过int范围,必须使用long long。

  2. 分割线位置理解:注意分割线是在行/列之间,不是在行/列上。

  3. 非空检查:确保分割后的两部分都至少包含一个元素。

  4. 边界条件:特别注意单行或单列矩阵的情况。

  5. 提前终止条件:当累加和超过target时及时终止,避免不必要计算。

9. 代码实现技巧

  1. 使用宏定义简化重复代码:

    #define CHECK_AND_BREAK(now, sum) \ if ((now) == (sum)) { \ return true; \ } \ if ((now) > (sum)) { \ break; \ }
  2. 使用范围for循环简化行遍历:

    for (auto& row : grid) { for (int num : row) { // ... } }
  3. 清晰的变量命名���使用sum、target、current等有意义的变量名。

  4. 模块化设计:将不同功能的代码块分离,提高可读性。

10. 其他语言实现示例

10.1 Python实现

def canPartitionGrid(grid): total = sum(sum(row) for row in grid) if total % 2 != 0: return False target = total // 2 # Check horizontal split current = 0 for row in grid: current += sum(row) if current == target: return True if current > target: break # Check vertical split current = 0 for j in range(len(grid[0])): current += sum(grid[i][j] for i in range(len(grid))) if current == target: return True if current > target: break return False

10.2 Java实现

class Solution { public boolean canPartitionGrid(int[][] grid) { long sum = 0; for (int[] row : grid) { for (int num : row) { sum += num; } } if (sum % 2 != 0) { return false; } long target = sum / 2; // Check horizontal split long current = 0; for (int[] row : grid) { for (int num : row) { current += num; } if (current == target) { return true; } if (current > target) { break; } } // Check vertical split current = 0; for (int j = 0; j < grid[0].length; ++j) { for (int i = 0; i < grid.length; ++i) { current += grid[i][j]; } if (current == target) { return true; } if (current > target) { break; } } return false; } }

11. 总结与个人体会

这道题目看似简单,但考察了多个编程基础能力:矩阵遍历、边界条件处理、算法优化等。在实际解决过程中,我有以下几点体会:

  1. 数学先行:先通过数学分析确定必要条件(总和为偶数),可以快速排除不可能的情况。

  2. 遍历顺序很重要:按行和按列两种遍历方式需要清晰区分,避免混淆。

  3. 提前终止是优化关键:一旦累加和超过目标值,后续检查就没有必要了。

  4. 边界条件容易忽略:特别是单行或单列矩阵的情况,需要特别注意。

  5. 变量类型选择:对于可能的大数,使用足够大的数据类型防止溢出。

通过这道题目,我更加理解了如何将数学思维与编程技巧结合,设计出高效的算法解决方案。在实际编程中,这种分析问题和优化算法的能力比单纯写出能运行的代码更为重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询