切披萨的方案数

标签: 记忆化搜索 数组 动态规划 矩阵

难度: Hard

给你一个 rows x cols 大小的矩形披萨和一个整数 k ,矩形包含两种字符: 'A' (表示苹果)和 '.' (表示空白格子)。你需要切披萨 k-1 次,得到 k 块披萨并送给别人。

切披萨的每一刀,先要选择是向垂直还是水平方向切,再在矩形的边界上选一个切的位置,将披萨一分为二。如果垂直地切披萨,那么需要把左边的部分送给一个人,如果水平地切,那么需要把上面的部分送给一个人。在切完最后一刀后,需要把剩下来的一块送给最后一个人。

请你返回确保每一块披萨包含 至少 一个苹果的切披萨方案数。由于答案可能是个很大的数字,请你返回它对 10^9 + 7 取余的结果。

示例 1:

输入:pizza = ["A..","AAA","..."], k = 3
输出:3 
解释:上图展示了三种切披萨的方案。注意每一块披萨都至少包含一个苹果。

示例 2:

输入:pizza = ["A..","AA.","..."], k = 3
输出:1

示例 3:

输入:pizza = ["A..","A..","..."], k = 1
输出:1

提示:

  • 1 <= rows, cols <= 50
  • rows == pizza.length
  • cols == pizza[i].length
  • 1 <= k <= 10
  • pizza 只包含字符 'A' 和 '.' 。

Submission

运行时间: 31 ms

内存: 16.2 MB

class Solution:
    def ways(self, pizza: List[str], k: int) -> int:
        R,C=len(pizza),len(pizza[0])
        f=[[0]*(C+1) for _ in range(R+1)]
        presums=[[0]*(C+1) for _ in range(R+1)]
        for r in range(R-1,-1,-1):
            for c in range(C-1,-1,-1):
                presums[r][c]=presums[r][c+1]+presums[r+1][c]+int(pizza[r][c]=='A')-presums[r+1][c+1]
                if presums[r][c]:
                    f[r][c]=1
        tot=presums[0][0]
        if tot<k:
            return 0

        # def get_apples(r1,c1,r2,c2):
        #     return presums[r2+1][c2+1]-presums[r2+1][c1]-presums[r1][c2+1]+presums[r1][c1]

        Mode=10**9+7
        for _ in range(k-1):
            cols=[0]*C
            for r in range(R-1,-1,-1):
                rows=0
                for c in range(C-1,-1,-1):
                    temp=f[r][c]
                    if presums[r][c]==presums[r+1][c]:
                        f[r][c]=f[r+1][c]
                    elif presums[r][c]==presums[r][c+1]:
                        f[r][c]=f[r][c+1]
                    else:
                        f[r][c]=(rows+cols[c])%Mode
                    rows+=temp
                    cols[c]+=temp
        return f[0][0]

Explain

这个题解使用动态规划的方法来解决切披萨问题。主要思路如下: 1. 首先预处理得到一个前缀和数组presums,其中presums[r][c]表示披萨从(r,c)位置到披萨右下角的苹果数量。 2. 然后定义一个二维DP数组f,其中f[r][c]表示从(r,c)位置到披萨右下角,切k-1次能够得到的合法方案数。 3. 初始化f数组,对于每个位置(r,c),如果从该位置到披萨右下角还有苹果,则f[r][c]=1,表示还可以进行一次合法切割。 4. 接下来进行k-1轮切割,每一轮都更新f数组。对于当前轮的每个位置(r,c),若该位置下方或右方已经没有苹果,则f[r][c]直接等于下方或右方的f值;否则,f[r][c]等于下方和右方所有合法切割方案数的总和。 5. 最后返回f[0][0]即为最终答案,表示从披萨左上角开始切k-1次能够得到的合法方案总数。

时间复杂度: O(kRC)

空间复杂度: O(RC)

class Solution:
    def ways(self, pizza: List[str], k: int) -> int:
        R,C=len(pizza),len(pizza[0])
        f=[[0]*(C+1) for _ in range(R+1)]  # DP数组,f[r][c]表示从(r,c)到右下角切k-1次的合法方案数
        presums=[[0]*(C+1) for _ in range(R+1)]  # 前缀和数组,presums[r][c]表示(r,c)到右下角的苹果数
        
        # 预处理前缀和数组
        for r in range(R-1,-1,-1):
            for c in range(C-1,-1,-1):
                presums[r][c]=presums[r][c+1]+presums[r+1][c]+int(pizza[r][c]=='A')-presums[r+1][c+1]
                if presums[r][c]:
                    f[r][c]=1  # 初始化f数组,如果(r,c)到右下角有苹果,则可以进行一次合法切割
                    
        tot=presums[0][0]  # 披萨中苹果的总数
        if tot<k:
            return 0  # 如果苹果数小于k,无法切成k块

        Mode=10**9+7
        for _ in range(k-1):  # 进行k-1轮切割
            cols=[0]*C  # 存储当前列的前缀和
            for r in range(R-1,-1,-1):
                rows=0  # 存储当前行的前缀和
                for c in range(C-1,-1,-1):
                    temp=f[r][c]
                    if presums[r][c]==presums[r+1][c]:  # 下方没有苹果
                        f[r][c]=f[r+1][c]
                    elif presums[r][c]==presums[r][c+1]:  # 右方没有苹果
                        f[r][c]=f[r][c+1]
                    else:
                        f[r][c]=(rows+cols[c])%Mode  # 当前位置的方案数为下方和右方的方案数之和
                    rows+=temp
                    cols[c]+=temp
        return f[0][0]  # 返回最终答案

Explore

前缀和数组`presums`的主要作用是快速计算任何子矩形区域内的苹果数量,以便在动态规划过程中判断是否可以在某个区域进行切割。使用`presums`可以在常数时间内获取任何从(r, c)到披萨右下角的区域内的苹果总数,这是因为通过前缀和,可以避免重复计算同一区域多次,从而有效地优化了算法的时间复杂度。

在更新DP数组`f`时,确保每块披萨至少含有一个苹果的关键在于使用`presums`数组。在每次切割更新时,通过检查`presums[r][c]`与`presums[r+1][c]`以及`presums[r][c+1]`的比较,确定切割后的每一部分至少包含一个苹果。此外,只有当`presums[r][c]`大于0时,位置`(r, c)`才会初始化为1,表示从该位置开始的区域内至少有一个苹果。这样的初始化和条件检查确保了每次更新时切割的有效性。

`if presums[r][c]`条件用于检查从坐标`(r, c)`到披萨的右下角是否存在至少一个苹果。如果`presums[r][c]`的值大于0,说明这个区域内有苹果,因此在动态规划数组`f`中,该位置`(r, c)`可以被初始化为1,表示从这个位置至少可以进行一次有效的切割。这是确保每次切割都至少包含一个苹果的基本前提。

在每一轮更新中,`f[r][c]`的值被更新为其下方`f[r+1][c]`和右方`f[r][c+1]`的值的和,但这只发生在苹果存在的情况下。具体来说,如果`(r, c)`位置到其下方没有苹果(`presums[r][c]`等于`presums[r+1][c]`),则`f[r][c]`更新为`f[r+1][c]`;如果到右方没有苹果(`presums[r][c]`等于`presums[r][c+1]`),则更新为`f[r][c+1]`。如果两边都有苹果,则`f[r][c]`更新为下方和右方的方案总和,即`f[r][c] = (f[r+1][c] + f[r][c+1]) % Mode`。这种更新保证了每一次切割都尽可能地增加方案数,同时确保每块披萨至少有一个苹果。