《程序员面试金典(第6版)》 面试题 08.13. 堆箱子(动态规划,与最长上升子序列问题相关的组合问题,C++)
Posted 阿宋同学
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《程序员面试金典(第6版)》 面试题 08.13. 堆箱子(动态规划,与最长上升子序列问题相关的组合问题,C++)相关的知识,希望对你有一定的参考价值。
题目描述
堆箱子。给你一堆n个箱子,箱子宽 wi、深 di、高 hi。箱子不能翻转,将箱子堆起来时,下面箱子的宽度、高度和深度必须大于上面的箱子。
实现一种方法,搭出最高的一堆箱子。箱堆的高度为每个箱子高度的总和。
输入使用数组[wi, di, hi]表示每个箱子。
示例1:
- 输入:box = [[1, 1, 1], [2, 2, 2], [3, 3, 3]]
- 输出:6
示例2:
- 输入:box = [[1, 1, 1], [2, 3, 4], [2, 6, 7], [3, 4, 5]]
- 输出:10
提示:
- 箱子的数目不大于3000个。
解题思路与代码
这道题的目的是,让我们去堆箱子,看n个箱子能够堆到的最大高度是多少。
但是呢,如何去堆这个箱子,它是有限定条件的。需要保证的是,整个箱子塔的结构,就和一个锥子一样。下面一层的箱子的长宽高,必须都大于上面一层箱子的长宽高,等于都不行。
拿到这道题的一瞬间,我脑子里面的思路是,这道题似乎可以用动态规划去解决它。果不其然,确实能用动态规划去解决。下面,就让我们看看,究竟该如何使用动态规划去解决这道题。
方法一: 动态规划 + 排序
首先我们先拿这个箱子的一个维度,去拍个序(降序)
。这样我们至少就是已经处理好一个维度的事情了。那么现在剩下的其实就是两个维度。到了两个维度以后呢,我们就可以很好的使用动态规划的思想去解决它啦~。
这里说一下,如果没有去降序,其实后面的操作,就会变得很麻烦,因为要后面要写动态规划的代码的话,必须要以一个某个维度最大的去作为基石,然后才能去逐步更新,直到最后解决问题。
我们拿动态规划的五步法,去解决这道题。
第一步,确定dp数组以及下标的含义:
- 我们定义一个一维的动态规划数组dp,其中dp[i]表示
以第i个箱子为底的最大堆箱子高度
。
第二步,确定推导公式(状态转移方程)
-
在求以第i个箱子为底的最大堆箱子高度时,需要我们去遍历箱子索引从 0 到 i - 1 的所有箱子,并且找到宽带、深度、高度都大于第i个箱子的箱子j,然后再去更新dp[i] 的值。
对于满足条件的箱子j,我们有:if(box[j][0] > box[i][0] && box[j][1] > box[i][1] && box[j][2] > box[i][2]) dp[i] = max(dp[i],dp[j] + box[i][2]);
dp[i] = max(dp[i], dp[j] + box[i][2])
;这个公式,就是我们的推导公式,也就是状态转移方程- 只要箱子j比箱子i大,我们就可以进行一次判定,到底是以i为底的dp[i]大,还是以j为底的dp[j]再加上一个箱子i更大
- 谁更大,那么更新后的dp[i]的值就等于谁。
第三步,初始化dp数组:
- 我们需要将 dp[i] 初始化为第 i 个箱子的高度,因为至少可以将每个箱子都单独成为一个箱子塔去堆放箱子:
dp[i] = box[i][2]
所以要用这样一个公式,去初始化dp数组。
第四步,确定遍历顺序:
-
首先,我们需要对箱子按照宽度、深度和高度的降序进行排序。这样,我们可以确保在遍历时,我们总是从一个更大的箱子到一个更小的箱子。
-
然后,我们从第一个箱子开始遍历,对于每个箱子 i,我们需要遍历所有小于 i 的箱子 j(即 j 从 0 到 i-1)。对于每个满足宽度、深度和高度都大于第 i 个箱子的箱子 j,我们根据递推公式更新 dp[i]。遍历完成后,dp 数组中的最大值就是最高的一堆箱子的高度。
第五步,举例推导dp数组
- 这一步的目的是让你先去纸上涂涂画画,看看自己有没有哪里可能出现了差错。检查步骤
这道题具体的代码如下:
class Solution
public:
int pileBox(vector<vector<int>>& box)
//降序排序箱子的高度
sort(box.begin(),box.end(),[](vector<int>& a, vector<int>& b)return a[2] > b[2];);
vector<int> dp(box.size(),0);
int maxHeight = 0;
for(int i = 0; i < box.size(); ++i)
dp[i] = box[i][2];
for(int j = 0; j < i; ++j)
if(box[j][0] > box[i][0] && box[j][1] > box[i][1] && box[j][2] > box[i][2])
dp[i] = max(dp[i],dp[j] + box[i][2]);
maxHeight = max(maxHeight,dp[i]);
return maxHeight;
;
复杂度分析
时间复杂度:
- 排序:sort 函数的时间复杂度是 O(n * log(n)),其中 n 是箱子的数量。
动态规划遍历:这段代码包含两层循环,外层循环遍历每个箱子,内层循环遍历当前箱子之前的所有箱子。因此,动态规划遍历的时间复杂度是 O(n^2)。
由于 O(n^2) 大于 O(n * log(n)),所以整段代码的时间复杂度是 O(n^2)。
空间复杂度:
- dp 数组:dp 数组的大小为 n,因此空间复杂度是 O(n)。
综上,这段代码的时间复杂度为 O(n^2),空间复杂度为 O(n)。
总结
说实话,这道题没有想象中的难。虽然说这是一道hard难度的题。但是我感觉它的难度像是middle,hhhhhh。可能是我变强了吧。继续加油,再接再厉。
《程序员面试金典(第6版)》面试题 08.01. 三步问题(动态规划,c++)
题目描述
三步问题。有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶或3阶。实现一种方法,计算小孩有多少种上楼梯的方式。结果可能很大,你需要对结果模1000000007。
示例1:
- 输入:n = 3
输出:4
说明: 有四种走法
示例2:
- 输入:n = 5
输出:13
提示:
- n范围在[1, 1000000]之间
解题思路与代码
这道题,我说实话对我而言没有什么太大的思路。然后我去网上看了看题解,主流的解法就是使用动态规划
去解题,还有用矩阵快速幂
去解题的,博主我大学数学学的不好,后悔大学没有好好学习高数,线代了,所以我这里先介绍动态规划的解法,到未来,如果我有一天数学基础又好了,我会回来将矩阵快速幂这个解法来补上的。
方法一,动态规划
这道题我们直接去套用Carl哥的动态规划五部曲
,去解题分析。
第一步,确定dp数组以及下标的含义:
- dp[i] : 爬到第i层楼,有dp[i]种方法。
第二步,确定递推(推导)公式
- 从dp[i] 的定义我们可以推导出,有三种方式可以得到dp[i],分别是:
- dp[i-1]代表的是,到达i-1层楼,一共有dp[i-1]种方法,那我再上一层楼,是不是就得到dp[i]了呢?
- dp[i-2]代表的是,到达i-2层楼,一共有dp[i-2]种方法,那我再上两层楼,是不是就得到dp[i]了呢?
- dp[i-3]代表的是,到达i-3层楼,一共有dp[i-3]种方法,那我再上三层楼,是不是就得到dp[i]了呢?
- 所以dp[i]就是dp[i-1],dp[i-2],dp[i-3]它们的和,
dp[i] = dp[i-1] + dp[i-2] + dp[i-3];
第三步,dp数组如何初始化?
- 首先,我认为所谓初始化,就是如何从最底层向结果去推演的一个过程,因为我们之前知道,一共有三种方式可以得到dp[i],所以我们等下就要初始化3个值。
- 那么由题意可值,n是一个正整数,最小值为1,这里讨论dp[0],就没有意义。
- 所以我们要从dp[1]开始初始化,所以dp[1] = 1,dp[2] = 1,dp[2] = 2,dp[3] = 4
第四步,确定遍历顺序
- 由递推公式:dp[i] = dp[i-1] + dp[i-2] + dp[i-3]; 我们可以看出,遍历顺序,一定是从前向后去遍历的。
第五步,举例推导dp数组
- 当 n 为 6 时,
- dp[1] = 1, dp[2] = 2, dp[3] = 4,
- dp[4] = dp[1] + dp [2] + dp[3] = 7,
- dp[5] = dp[2] + dp [3] + dp[4] = 13,
- dp[6] = dp[3] + dp [4] + dp[5] = 24.
当这五部曲分析完成后,我们就可以去写代码啦~
代码如下:
class Solution
public:
int waysToStep(int n)
if(n <= 2) return n;
if(n == 3) return 4;
vector<int> dp (n+1,0);
dp[1] = 1; dp[2] = 2; dp[3] = 4;
for(int i = 4; i < n+1; ++i)
dp[i] = ((dp[i-1] + dp[i-2])%1000000007 + dp[i-3])%1000000007; //这个公式是由下面公式合并同类项而来的
//dp[i] = ((dp[i-3]%1000000007 + dp[i-2])%1000000007 + dp[i-1]%1000000007)%1000000007;
return dp[n];
;
复杂度分析
时间复杂度分析:
- 初始化一个长度为 n+1 的 dp 数组,时间复杂度为 O(n)。
遍历 dp 数组,计算 dp[i] 的值,时间复杂度为 O(n)。
整个算法的时间复杂度为 O(n)。
空间复杂度分析:
- 开辟一个长度为 n+1 的 dp 数组,空间复杂度为 O(n)。
综上所述,该算法的时间复杂度为 O(n),空间复杂度为 O(n)。需要注意的是,由于取模操作的存在,实际运行时间可能会略微慢一些,但不会改变时间复杂度的量级。
方法二,使用滚动遍历,优化动态规划
和上题的思想一样,只不过用4个int变量,去替代了dp数组。
int one = 1,代替了原来的dp[1], int two = 2,代替了原来的dp[2], int three = 4,代替了原来的dp[3],
int result = ((three + two)%1000000007 + one)%1000000007,代替了原来的dp[n]
然后用one = two ,two = three,three = result,去不断更新dp[i-1],dp[i-2],dp[i-3]的值。
具体的代码实现如下:
class Solution
public:
int waysToStep(int n)
if(n <= 2) return n;
if(n == 3) return 4;
int one = 1; int two = 2; int three = 4; int result = 0;
for(int i = 4; i < n+1; ++i)
result = ((three + two)%1000000007 + one)%1000000007;
one = two;
two = three;
three = result;
return result;
;
复杂度分析
时间复杂度分析:
初始化三个变量 one, two, three,时间复杂度为 O(1)。
遍历 n 次,计算 result 的值,时间复杂度为 O(n)。
整个算法的时间复杂度为 O(n)。
空间复杂度分析:
只开辟了三个变量 one, two, three,空间复杂度为 O(1)。
综上所述,该算法的时间复杂度为 O(n),空间复杂度为 O(1)。
总结
这道题,是一道动态规划非常好的练手题。我们可以拿它来做动态规划的入门。很好很不错!
以上是关于《程序员面试金典(第6版)》 面试题 08.13. 堆箱子(动态规划,与最长上升子序列问题相关的组合问题,C++)的主要内容,如果未能解决你的问题,请参考以下文章
《程序员面试金典(第6版)》面试题 08.04. 幂集(回溯算法,位运算,C++)不断更新
《程序员面试金典(第6版)》面试题 08.08. 有重复字符串的排列组合(回溯算法,全排列问题)C++