第201题 小M的蛋糕切割问题

106 阅读4分钟

问题描述

小M有一个 n×mn×m 的矩形蛋糕,蛋糕被分为 n×mn×m 个区域,每个区域都有一个美味度。她希望通过一刀将蛋糕切成两部分,一部分自己吃,另一部分给小团。切下的区域必须是完整的,即不能把某个小正方形切成两个小区域。

小M希望两个人吃的部分的美味度之和尽量接近。你的任务是计算出美味度差的最小值,即 s1s2∣s1−s2∣ 的最小值,其中 s1s1 是小M吃到的美味度,s2s2 是小团吃到的美味度。

题目分析

我们需要将一个 n×mn \times m 的矩形蛋糕用一刀切成两部分,使得两部分的美味度之和尽可能接近。具体来说,要计算两部分美味度的差值 s1s2|s_1 - s_2| 的最小值,其中 s1s_1 是一部分美味度,s2s_2 是另一部分美味度。

切割规则

  1. 只能通过横向或纵向切割蛋糕。
  2. 切割必须经过整行或整列,不能把某个区域分成碎片。

目标: 找到最小的 s1s2∣s1−s2∣


解题思路

核心思想
在横向和纵向切割中,逐一尝试每一种切割方式,计算两部分美味度的差值并记录最小值。

步骤1:前缀和的构造

为了快速计算任意区域的美味度,我们使用前缀和:

  1. 行前缀和: 每一行的前缀和数组存储该行从左到当前位置的美味度总和。

    • 公式:
      row_prefix[i][j+1]=row_prefix[i][j]+a[i][j]{row\_prefix}[i][j+1] = {row\_prefix}[i][j] + \text{a}[i][j]
    • 效果:快速获取第 ii 行从第1列到第 jj 列的美味度。
  2. 列前缀和: 每一列的前缀和数组存储该列从上到当前位置的美味度总和。

    • 公式:
      col_prefix[j][i+1]=col_prefix[j][i]+a[i][j]{col\_prefix}[j][i+1] = {col\_prefix}[j][i] + \text{a}[i][j]
    • 效果:快速获取第 jj 列从第1行到第 ii 行的美味度。

步骤2:尝试横向切割

对于横向切割,在每一行之间切割,切割后的两部分分别为:

  • 上半部分:前 ii 行的美味度总和。
  • 下半部分:剩余行的美味度总和。

通过总美味度和上半部分美味度,可以快速计算下半部分美味度:

  • bottom_sum=total_sumtop_sum{bottom\_sum} = {total\_sum} - {top\_sum}

记录美味度差值的绝对值 s1s2∣s1−s2∣,更新最小值。

步骤3:尝试纵向切割

对于纵向切割,在每一列之间切割,切割后的两部分分别为:

  • 左半部分:前 jj 列的美味度总和。
  • 右半部分:剩余列的美味度总和。

通过总美味度和左半部分美味度,可以快速计算右半部分美味度:

  • right_sum=total_sumleft_sum{right\_sum} = {total\_sum} - {left\_sum}

记录美味度差值的绝对值 s1s2∣s1−s2∣,更新最小值。

步骤4:输出结果

在横向和纵向切割中找到的最小差值即为答案。


代码实现

def solution(n: int, m: int, a: list) -> int:
    # 计算行前缀和和列前缀和
    row_prefix = [[0] * (m + 1) for _ in range(n)]
    col_prefix = [[0] * (n + 1) for _ in range(m)]

    # 填充前缀和
    for i in range(n):
        for j in range(m):
            row_prefix[i][j + 1] = row_prefix[i][j] + a[i][j]
            col_prefix[j][i + 1] = col_prefix[j][i] + a[i][j]

    # 蛋糕总美味度
    total_sum = sum(row_prefix[i][m] for i in range(n))

    # 初始化最小差值
    min_diff = float('inf')

    # 横向切割
    for i in range(1, n):  # 从第1行开始尝试切割
        top_sum = sum(row_prefix[k][m] for k in range(i))  # 上半部分美味度
        bottom_sum = total_sum - top_sum  # 下半部分美味度
        min_diff = min(min_diff, abs(top_sum - bottom_sum))

    # 纵向切割
    for j in range(1, m):  # 从第1列开始尝试切割
        left_sum = sum(col_prefix[k][n] for k in range(j))  # 左半部分美味度
        right_sum = total_sum - left_sum  # 右半部分美味度
        min_diff = min(min_diff, abs(left_sum - right_sum))

    return min_diff


if __name__ == '__main__':
    assert solution(2, 3, [[1, 1, 4], [5, 1, 4]]) == 0
    assert solution(3, 3, [[3, 2, 1], [4, 5, 6], [7, 8, 9]]) == 3
    assert solution(2, 2, [[1, 2], [3, 4]]) == 2

时间复杂度分析

  1. 前缀和计算

    • 遍历 n×mn \times m 的矩阵,时间复杂度为 O(n×m)O(n \times m)
  2. 横向切割

    • 遍历所有行,计算累积和,时间复杂度为 O(n)O(n)
  3. 纵向切割

    • 遍历所有列,计算累积和,时间复杂度为 O(m)O(m)

总时间复杂度为: O(n×m)+O(n)+O(m)=O(n×m)O(n \times m) + O(n) + O(m) = O(n \times m)


空间复杂度分析

  1. 使用了两个辅助数组存储前缀和,空间复杂度为:

    • 行前缀和:O(n×(m+1))O(n \times (m+1))
    • 列前缀和:O(m×(n+1))O(m \times (n+1))

空间复杂度为 O(n×m)O(n \times m)


测试样例解析

样例1:

输入:

n = 2, m = 3, a = [[1, 1, 4], [5, 1, 4]]

总美味度:1616
最佳切割位置在第二列和第三列之间,左右部分美味度分别为 8888
输出:00

样例2:

输入:

n = 3, m = 3, a = [[3, 2, 1], [4, 5, 6], [7, 8, 9]]

总美味度:4545
最佳切割位置在第二行和第三行之间,上下部分美味度分别为 21212424
输出:33

样例3:

输入:

n = 2, m = 2, a = [[1, 2], [3, 4]]

总美味度:1010
最佳切割位置在第一列和第二列之间,左右部分美味度分别为 4466
输出:22


总结

该方法使用前缀和优化了美味度的计算,遍历所有可能的切割位置并找到最优解。