卖木头块

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

难度: Hard

给你两个整数 m 和 n ,分别表示一块矩形木块的高和宽。同时给你一个二维整数数组 prices ,其中 prices[i] = [hi, wi, pricei] 表示你可以以 pricei 元的价格卖一块高为 hi 宽为 wi 的矩形木块。

每一次操作中,你必须按下述方式之一执行切割操作,以得到两块更小的矩形木块:

  • 沿垂直方向按高度 完全 切割木块,或
  • 沿水平方向按宽度 完全 切割木块

在将一块木块切成若干小木块后,你可以根据 prices 卖木块。你可以卖多块同样尺寸的木块。你不需要将所有小木块都卖出去。你 不能 旋转切好后木块来交换它的高度值和宽度值。

请你返回切割一块大小为 m x n 的木块后,能得到的 最多 钱数。

注意你可以切割木块任意次。

示例 1:

输入:m = 3, n = 5, prices = [[1,4,2],[2,2,7],[2,1,3]]
输出:19
解释:上图展示了一个可行的方案。包括:
- 2 块 2 x 2 的小木块,售出 2 * 7 = 14 元。
- 1 块 2 x 1 的小木块,售出 1 * 3 = 3 元。
- 1 块 1 x 4 的小木块,售出 1 * 2 = 2 元。
总共售出 14 + 3 + 2 = 19 元。
19 元是最多能得到的钱数。

示例 2:

输入:m = 4, n = 6, prices = [[3,2,10],[1,4,2],[4,1,3]]
输出:32
解释:上图展示了一个可行的方案。包括:
- 3 块 3 x 2 的小木块,售出 3 * 10 = 30 元。
- 1 块 1 x 4 的小木块,售出 1 * 2 = 2 元。
总共售出 30 + 2 = 32 元。
32 元是最多能得到的钱数。
注意我们不能旋转 1 x 4 的木块来得到 4 x 1 的木块。

提示:

  • 1 <= m, n <= 200
  • 1 <= prices.length <= 2 * 104
  • prices[i].length == 3
  • 1 <= hi <= m
  • 1 <= wi <= n
  • 1 <= pricei <= 106
  • 所有 (hi, wi) 互不相同 。

Submission

运行时间: 145 ms

内存: 21.7 MB

class Solution:
    def sellingWood(self, m: int, n: int, prices: List[List[int]]) -> int:
        """
        动态规划
        dp[x,y]表示 高、宽为x,y时可以得到的最多钱数
        
        情况1: 当x,y存在于prices中的木块大小
            dp[xi,yi] = prices[i]

        情况2: 当高x>1时,可以水平切割
            dp[x,y] = max(dp[i,y]+dp[x-i, y], x in [1, x-1])
        情况3: 当宽y>1时,可以垂直切割
            dp[x,y] = max(dp[x,j]+dp[x, y-j]), y in [1, y-1])
        """

        # vals = {}
        # @cache
        # def dfs(x, y):
        #     ret = vals.get((x,y), 0)

        #     if x > 1:
        #         for i in range(1, x//2+1):
        #             ret = max(ret, dfs(i, y)+dfs(x-i, y))

        #     if y > 1:
        #         for j in range(1, y//2+1):
        #             ret = max(ret, dfs(x, j) + dfs(x, y-j))
        #     return ret
        dp = [ [0]*(n+1) for _ in range(m+1)]

        for (h, w, price) in prices:
            dp[h][w] = price
        
        ret = 0
        optimized_split_height_per_width = [[] for _ in range(n+1)]
        for i in range(1, m+1):
            optimized_split_width_per_height = []
            for j in range(1, n+1):
                tmp = dp[i][j]
                height_optimized = True
                width_optimized = True
                # 水平切割,剪枝,并不是所有位置都需要枚举
                # 只需要枚举之前的最后分割点即可,之前没有进行分割的点,后面用不到
                for split_h in optimized_split_height_per_width[j]:
                    val = dp[split_h][j]+dp[i-split_h][j]
                    if val > tmp:
                        # 当前宽高是[i, j],可以进行水平分割
                        # 分割高度是split_h
                        # 记录下来,当高为i时,可以枚举这个宽度
                        tmp = val
                        height_optimized = False
                        width_optimized = True
                    elif val == tmp:
                        height_optimized = False

                # 垂直切割,剪枝,并不是所有位置都需要枚举
                for split_w in optimized_split_width_per_height:
                    val = dp[i][split_w]+dp[i][j-split_w]
                    if val > tmp:
                        # 当前宽高是[i, j],可以进行垂直分割
                        # 分割宽度是split_w
                        # 记录下来,当宽为j时,可以枚举这个高度
                        tmp = val
                        width_optimized = False
                        height_optimized = True
                    elif val == tmp:
                        width_optimized = False
                if tmp == 0:
                    width_optimized = height_optimized = False
                dp[i][j] = tmp
                if width_optimized:
                    optimized_split_width_per_height.append(j)
                if height_optimized:
                    optimized_split_height_per_width[j].append(i)

        ret = dp[-1][-1]
        return ret

Explain

此题采用动态规划的方法来解决。定义 dp[x][y] 为当木块尺寸为 x 高和 y 宽时可以获得的最大收益。初始化 dp 数组时,先将所有给定的价格设置到对应的尺寸上。接下来,对于每个尺寸的木块,尝试进行水平和垂直的切割,更新 dp[x][y] 为所有可能的切割方式中的最大值。通过维护 optimized_split_height_per_width 和 optimized_split_width_per_height 两个列表来记录对于每个尺寸的木块,之前有效的切割位置,从而减少不必要的重复计算,优化动态规划过程。最终 dp[m][n] 存储的就是切割大小为 m x n 的木块能得到的最多钱数。

时间复杂度: O(m^2 * n + m * n^2)

空间复杂度: O(m * n)

class Solution:
    def sellingWood(self, m: int, n: int, prices: List[List[int]]) -> int:
        # Initialize dp table with all zeros
        dp = [[0]*(n+1) for _ in range(m+1)]

        # Set prices in dp table
        for (h, w, price) in prices:
            dp[h][w] = price
        
        # Initialize lists to store optimized split positions
        optimized_split_height_per_width = [[] for _ in range(n+1)]
        for i in range(1, m+1):
            optimized_split_width_per_height = []
            for j in range(1, n+1):
                tmp = dp[i][j]
                height_optimized = width_optimized = True
                # Horizontal cuts
                for split_h in optimized_split_height_per_width[j]:
                    val = dp[split_h][j]+dp[i-split_h][j]
                    if val > tmp:
                        tmp = val
                        height_optimized = False
                    elif val == tmp:
                        height_optimized = False

                # Vertical cuts
                for split_w in optimized_split_width_per_height:
                    val = dp[i][split_w]+dp[i][j-split_w]
                    if val > tmp:
                        tmp = val
                        width_optimized = False
                    elif val == tmp:
                        width_optimized = False
                
                # Update dp table and optimized lists
                dp[i][j] = tmp
                if width_optimized:
                    optimized_split_width_per_height.append(j)
                if height_optimized:
                    optimized_split_height_per_width[j].append(i)

        # Return the maximum possible money from cutting m x n wood block
        return dp[m][n]

Explore

这两个列表的目的是为了存储之前有效的切割位置,从而减少不必要的重复计算。在动态规划中,每次尝试切割一个木块时,不需要遍历所有可能的切割位置,而是只考虑之前已经证明能提供最大收益的切割位置。这样做可以优化动态规划的过程,提高算法的效率。例如,如果在某个尺寸的木块中,某一切割位置未曾提供超越已知最大收益的新收益,则在后续相同的切割尝试中可以忽略这个切割位置。

在初始化dp表时,如果存在多个价格条目针对同一尺寸,应该选择最高的价格设置到dp表中。这是因为我们的目标是最大化收益,因此对于同一尺寸的木块,应当采用能够获得最大收益的价格。在代码中,可以通过比较当前价格与dp表中已存储的价格,选择较大者进行更新。

在动态规划中通过考虑所有可能的水平和垂直切割,我们尝试找到任何可能的方式来最大化收益。理论上,通过穷尽这些切割方式,我们能够确保找到最优解。然而,动态规划的前提是基于'最优子结构'的假设——即问题的最优解包含了其子问题的最优解。如果这个假设成立,我们的方法总是能找到最优解。不过,如果存在某些特殊情况或约束(例如价格不规则或者受限的切割规则),那么可能需要对算法进行调整或增加额外的逻辑来处理这些特殊情况。