第37天,动态规划part05,开始完全背包问题(ง •_•)ง,编程语言:C++
目录
完全背包理论基础
文档讲解:代码随想录完全背包理论基础
视频讲解:手撕完全背包
完全背包问题指的是:有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
可以见完全背包问题与0-1背包问题,不同点就在于,完全背包问题中物品的数量是无限的。
因此对于完全背包问题的求解来说主要的不同点就在于递推公式上。
动规五部曲
1.确定dp数组以及下标含义:与0-1背包相同,我们同样可以采用二维数组,或者一维滚动数组的方式,来确定dp数组。dp[i][j]就表示为j容量下,从[0-i]中拿物品能够得到的最大价值,dp[j]同理。
2.确定递推公式:相较于0-1背包,dp[i][j]依靠的是dp[i - 1][j] 和 dp[i - 1][j - weight[i]]。完全背包由于可以重复取用物品 i 因此,dp[i][j]依靠的不再是左上方的节点,而是其左边的节点,且其左边的节点已完成对物品[i]的取用,这样我们才能保证可以重复的取用物品[i]。因此完全背包的递推公式为:dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i])。而对于一维滚动数组而言递推公式则没有变化:dp[j] = max(dp[j], dp[j - weight[i] + value[i])。虽然一维滚动数组的递推公式没有变化,但我们从二维dp数组的递推公式可以看出,我们需要借用左边的结果,因此,对于一维滚动数组来说,他必须从前往后遍历,与0-1背包的从后往前遍历不同。
3.初始化dp数组:这个与0-1背包相同。
4.遍历顺序:一维滚动数组的遍历顺序与0-1背包不同,需要从前往后开始遍历。
// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
5.举例dp数组:不同例子单独分析,与0-1背包相同。
注意:对于遍历顺序来说,物品和背包的遍历顺序,在纯完全背包问题中是可以交换位置的,其实也很好理解,我们实际上是在遍历物品的不同组合带来的最大值,先遍历物品在遍历背包得到的就是不考虑顺序的组合结果,并从中找到最大值。而先遍历背包再遍历物品就是考虑了顺序排列组合并找到其中的最大值。具体可以用一个例子实际推导。
卡玛网52题:
题目:卡玛网题目链接
学习:此题就是标准的完全背包问题,按照要求进行动归五部曲即可。
代码:
#include<iostream>
#include<vector>
using namespace std;
int backcomplete(int N, int V, vector<int>& weight, vector<int>& value) {
//1.确定dp数组以及下标含义
vector<int> dp(V + 1, 0); //dp[j]表示j容量下能够存下的最大价值。
//2.确定递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
//3.初始化dp数组,dp[0] = 0;
//4.确定遍历顺序:先遍历物品再遍历背包
for(int i = 0; i < N; i++) {
for(int j = weight[i]; j <= V; j++) {
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
return dp[V];
}
int main() {
int N, V;
cin >> N; //研究材料的种类
cin >> V; //行李空间
vector<int> weight(N);
vector<int> value(N);
for(int i = 0; i < N; i++) {
cin >> weight[i]; //输入物品重量
cin >> value[i]; //输入物品价值
}
int maxvalue = backcomplete(N, V, weight, value);
cout << maxvalue;
system("pause");
return 0;
}
518.零钱兑换II
文档讲解:代码随想录零钱兑换II
视频讲解:手撕零钱兑换II
题目:力扣题目链接
学习: 本题规定了硬币数量有无限个,且规定了总金额数,因此本题可以采用完全背包的方法。(实际上本题也可以采用回溯的方法进行组合数的求解)。
从动规五部曲出发:
1.确定dp数组以及下标的含义:与纯dp数组不同,本题要找的是组合数,类似于0-1背包问题中的目标和。因此从需求出发,我们可以定义一个一维dp数组,dp[j]表示为凑成总和 j 的货币最大组合数。
2.确定递推公式:dp[j]实际就是所有的dp[j - coins[i]]结果相加。dp[j] += dp[j - coin[i]]。
3.初始化dp数组:对于除0以外的我们可以初始化为0,对于dp[0]来说,举个例子,只有一个硬币面值为1,则我们求得结果dp[1]应该是1,而dp[1] += dp[1 - 1] = dp[0],因此dp[0]需要等于1,就表示为一种方法。在这里可能有两种解释:一种就是上面说的,为了满足递推公式而服务,且确实满足了实际的情况;另一种则是表示凑成总和0的方法,就是不选硬币,这也是一种方法。
4.确定遍历顺序,本题是不考虑元素之间的顺序的,也就是组合问题而非排列问题,因此本题需要先遍历物品再遍历背包。
5.举例推导dp数组:
代码:
//时间复杂度O(mn)
//空间复杂度O(m)
class Solution {
public:
int change(int amount, vector<int>& coins) {
//每种硬币无限取,规定了总额数,因此属于完全背包问题
//1.确定dp数组以及下标含义
vector<int> dp(amount + 1, 0); //dp[j]表示总和为j的情况下,能够凑成j的种类数
//2.确定递推公式:dp[j] += dp[j - coins[i]];
dp[0] = 1;
//4.确定遍历顺序
for(int i = 0; i < coins.size(); i++) {
for(int j = coins[i]; j <= amount; j++) {
if(j == coins[i]) {
dp[j] += 1;
}
else {
dp[j] += dp[j - coins[i]];
}
}
}
return dp[amount];
}
};
377.组合总和IV
文档讲解:代码随想录组合总和IV
视频讲解:手撕组合总和IV
题目:力扣题目链接
学习:本题与上一题可以说是完全相同,给的nums数组就类似硬币数组,总和就是target。不同点在于本题求得是排列组合数,需要考虑顺序,换言之,本题与上一题不同之处只在于白能力顺序不同,交换遍历顺序即可。
注意:但还需要注意,本题题目数据只保证了最后的答案符合32位整数范围,不代表中间不会出现大于整数的可能性,因此还需要对此进行判断。
代码:
//时间复杂度O(target*n)
//空间复杂度O(target)
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
//完全背包问题:数量无限,有容量值
//1.确定dp数组以及下标含义
vector<int> dp(target + 1, 0); //dp[j]表示总和j的情况下,排列种类为dp[j];
//2.确定递推公式:dp[j] += dp[j - nums[i]];
//3.初始化dp数组
dp[0] = 1;
//4.遍历顺序
for(int j = 0; j <= target; j++) {
for(int i = 0; i < nums.size(); i++) {
if(j >= nums[i] && dp[j] <= INT_MAX - dp[j-nums[i]]) { //保证数据在32位整数范围内
dp[j] += dp[j - nums[i]];
}
}
}
return dp[target];
}
};
卡玛网57.爬楼梯(进阶)
文档讲解:代码随想录爬楼梯
题目:卡玛网题目链接
学习:本题是爬楼梯的变种,一般的爬楼梯题目规定了只能趴一阶或者二阶楼梯,但是本题最多可以爬m阶楼梯。但其实仔细分析本题中每一次可以爬1-m阶楼梯实际上也就是“物品的种类”,而爬到n层就是“背包的容量”,找到有多少种方法,就是找到能够满足“背包的容量“的不同排列方法。
因此本题的解题思路与上一题组合总和IV来说一模一样。
注意:但需要注意的是,本题需要采用ACM编码方式,注意库的导入和数据的引入方式。
代码:
//时间复杂度O(n*m)
//空间复杂度O(n)
#include<iostream>
#include<vector>
using namespace std;
int main() {
int m, n;
cin >> n >> m; //m:最多爬m阶台阶,n:需要爬n阶台阶才能到达楼顶
//1.确定dp数组以及下标含义
vector<int> dp(n + 1, 0); //dp[j]:表示爬j阶台阶的所有排列总数
//2.确定递推公式dp[j] += dp[j - i];
//3.初始化dp数组
dp[0] = 1;
//4.确定遍历顺序
for(int j = 1; j <= n; j++) {
for(int i = 1; i <= m && i <= j; i++) {
dp[j] += dp[j - i];
}
}
cout << dp[n];
system("pause");
return 0;
}
总结:
今天主要讲解了完全背包的类型、解题方法。以及举出了三种不同的情形,与0-1背包相同,很多题目并不会直接的表现出纯完全背包的样子,我们要注意两点:1.是否元素能够无限取;2.是否有某个量的限制要求。如果有以上两点,就尝试用用完全背包的方法吧。