【C/C++】2312. 卖木头块

173 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情


题目链接:2312. 卖木头块

题目描述

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

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

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

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

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

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

提示:

  • 1m,n2001 \leqslant m, n \leqslant 200
  • 1prices.length21041 \leqslant prices.length \leqslant 2 * 10^4
  • prices[i].length==3prices[i].length == 3
  • 1him1 \leqslant h_i \leqslant m
  • 1win1 \leqslant w_i \leqslant n
  • 1pricei1061 \leqslant price_i \leqslant 10^6
  • 所有 (hi,wi)(h_i, w_i) 互不相同 。

示例 1:

ex1.png

输入: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:

ex2new.png

输入: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 的木块。

整理题意

题目给定一块大小为 m * n 的木板,以及能够卖钱的木板大小和售卖价格 prices ,其中 prices[i]=[hi,wi,pricei]prices[i] = [h_i, w_i, price_i] 表示你可以以 priceiprice_i 元的价格卖一块高为 hih_i 宽为 wiw_i 的矩形木板。

题目允许切割木板,但切割有要求,只能垂直和水平切割,且必须 完全 切割,也就是一刀且到底,切完的木板任然是个矩形。

此外在卖木板的时候不能旋转,也就是 m * n 的木板 不能n * m 的价格售卖。

问当前大小为 m * n 的木板最多能够卖多少钱。

题目规定可以切割木块任意多次

解题思路分析

  • 对于一块木板,通过垂直或水平方向切割后会得到两块更小的木板,也就得到了两个更小的子问题。求得两块小木板能够得到的最多钱数量,也就知道了大木块能够得到的最多钱数量,也就是求 最优子结构
  • 「先水平切割再垂直切割」和「先垂直切割再水平切割」是可以得到相同的木板,也就是殊途同归的,也就是存在 重叠的子问题
  • 对于高度和宽度相等的木板,能得到最多的钱数量是相等的,也就是与切法无关,无后效性。 以上分析满足使用 动态规划 的条件。

具体实现

  1. 定义状态:dp[i][j] 表示高度为 i,长度为 j 的木板能够得到的最多钱数量。
  2. 初始化边界状态:出现在 prices 数组中的木块大小和售卖价格为初始状态,未出现的木块大小售卖价格为 0
  3. 状态转移:
    • 直接卖:dp[i][j];
    • 枚举水平切割位置 k,找到最大收益的切割位置:dp[k][j] + dp[i - k][j]k = [1, i)
    • 枚举垂直切割位置 k,找到最大收益的切割位置:dp[i][k] + dp[i][j - k]k = [1, j)
    • 上述三种情况取最大值即为 dp[i][j]
  4. 最后返回 dp[m][n] 即为答案。

优化

  • 根据对称性,枚举切割位置 k 时,仅需举到一半的位置即可;
  • 由于我们时从小到大递推计算 dp[i][j] 的,所以可以直接将初始化边界状态的 prices 记录到 dp[i][j] 中去,而不会印象最终计算结果。

复杂度分析

  • 时间复杂度:O(mn(m+n))O(mn(m+n))m 为木板高度,n 为木板宽度。
  • 空间复杂度:O(mn)O(mn)

代码实现

需要注意能够得到的最多钱数量是会溢出 int 存储范围的,需要使用更大的 long long int 进行存储。

class Solution {
public:
    long long sellingWood(int m, int n, vector<vector<int>>& prices) {
        //递推状态定义:dp[i][j]表示高度为i宽度为j的木块能够得到的最多钱数
        long long int dp[m + 1][n + 1];
        //初始化边界状态
        memset(dp, 0, sizeof(dp));
        for(auto &p : prices) dp[p[0]][p[1]] = p[2];
        for(int i = 1; i <= m; i++){
            for(int j = 1; j <= n; j++){
                //直接卖的价格为 dp[i][j];
                //枚举水平和垂直方向切割位置
                for(int k = 1; k <= i / 2; k++) dp[i][j] = max(dp[i][j], dp[k][j] + dp[i - k][j]);
                for(int k = 1; k <= j / 2; k++) dp[i][j] = max(dp[i][j], dp[i][k] + dp[i][j - k]);
            }
        }
        return dp[m][n];
    }
};

总结

  • 使用定长数组比变长数组 vector 在速度上会快很多。
  • 在考虑使用「记忆化搜索递归」和「动态规划递推」的性能方面时,如果所有状态都要计算一遍,那么「动态规划递推」会更快一点;如果很多状态计算不到的话,那么「记忆化搜索递归」更快,因为有个函数调用的开销。本题需要遍历所有状态,「动态规划递推」更快
  • 如果题目满足:①最优子结构②重叠的子问题③无后效性,这三个条件时可以考虑使用 动态规划 进行解题。
  • 测试结果:

2312.png

2312-及优化.png 可以看到优化后的效果还是很明显的。

结束语

我们都会遇到困难和挑战,甚至有时候会绝望、想放弃。然而,只有经得起今天的沮丧,才能收获明天的喜悦。不管前路多么崎岖,只要走的方向正确,都比站在原地更接近幸福。新的一天,加油!