携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情
题目链接:2312. 卖木头块
题目描述
给你两个整数 m 和 n ,分别表示一块矩形木块的高和宽。同时给你一个二维整数数组 prices ,其中 表示你可以以 元的价格卖一块高为 宽为 的矩形木块。
每一次操作中,你必须按下述方式之一执行切割操作,以得到两块更小的矩形木块:
- 沿垂直方向按高度 完全 切割木块,或
- 沿水平方向按宽度 完全 切割木块
在将一块木块切成若干小木块后,你可以根据 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 的木块。
整理题意
题目给定一块大小为 m * n 的木板,以及能够卖钱的木板大小和售卖价格 prices ,其中 表示你可以以 元的价格卖一块高为 宽为 的矩形木板。
题目允许切割木板,但切割有要求,只能垂直和水平切割,且必须 完全 切割,也就是一刀且到底,切完的木板任然是个矩形。
此外在卖木板的时候不能旋转,也就是 m * n 的木板 不能 以 n * m 的价格售卖。
问当前大小为 m * n 的木板最多能够卖多少钱。
题目规定可以切割木块任意多次
解题思路分析
- 对于一块木板,通过垂直或水平方向切割后会得到两块更小的木板,也就得到了两个更小的子问题。求得两块小木板能够得到的最多钱数量,也就知道了大木块能够得到的最多钱数量,也就是求 最优子结构。
- 「先水平切割再垂直切割」和「先垂直切割再水平切割」是可以得到相同的木板,也就是殊途同归的,也就是存在 重叠的子问题。
- 对于高度和宽度相等的木板,能得到最多的钱数量是相等的,也就是与切法无关,无后效性。 以上分析满足使用 动态规划 的条件。
具体实现
- 定义状态:
dp[i][j]表示高度为i,长度为j的木板能够得到的最多钱数量。 - 初始化边界状态:出现在
prices数组中的木块大小和售卖价格为初始状态,未出现的木块大小售卖价格为0。 - 状态转移:
- 直接卖:
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]。
- 直接卖:
- 最后返回
dp[m][n]即为答案。
优化
- 根据对称性,枚举切割位置
k时,仅需举到一半的位置即可; - 由于我们时从小到大递推计算
dp[i][j]的,所以可以直接将初始化边界状态的prices记录到dp[i][j]中去,而不会印象最终计算结果。
复杂度分析
- 时间复杂度:,
m为木板高度,n为木板宽度。 - 空间复杂度:。
代码实现
需要注意能够得到的最多钱数量是会溢出
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在速度上会快很多。 - 在考虑使用「记忆化搜索递归」和「动态规划递推」的性能方面时,如果所有状态都要计算一遍,那么「动态规划递推」会更快一点;如果很多状态计算不到的话,那么「记忆化搜索递归」更快,因为有个函数调用的开销。本题需要遍历所有状态,「动态规划递推」更快
- 如果题目满足:①最优子结构、②重叠的子问题、③无后效性,这三个条件时可以考虑使用 动态规划 进行解题。
- 测试结果:
可以看到优化后的效果还是很明显的。
结束语
我们都会遇到困难和挑战,甚至有时候会绝望、想放弃。然而,只有经得起今天的沮丧,才能收获明天的喜悦。不管前路多么崎岖,只要走的方向正确,都比站在原地更接近幸福。新的一天,加油!