完全背包
例题:有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
区别:完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
公式推导:01背包问题是从二维数组压缩到一维数组,由于一维数组要保证每次取得物品不重合,遍历顺序也就变成了从后向前遍历。但是完全背包问题每种物品数量不限制数量,也就是说不需要防止重合情况,那问题就变成了一维数组或者说是滚动数组从前往后遍历。
首先再回顾一下01背包的核心代码
for(int i = 0; i < weight.size(); i++) { // 遍历物品for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);}
}
而完全背包的物品是可以添加多次的,所以要从小到大去遍历
// 先遍历物品,再遍历背包
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]);}
}
注意:这里j什么要从weight[i]开始,因为当j小于weight[i]时,也就是当前容量不足以装下当前第i个物品,所以每次在遍历开始都要从weight[i]开始。
dp数组初始化:dp[j]=0,也就是没放物品时,不管容量多大,价值都是0;
举例推导dp公式:
背包最大重量为4。
物品为:
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
每件商品都有无限个!
问背包能背的物品最大价值是多少?
我们来走一遍流程:
只可以选物品0时:随着容量变大,不断放进物品0就行,直到装满,结果就出来了 ;
可以选物品0和物品1时:首先容量0-2时,物品1放不进,所有直接将上一步结果放入,这一步其实也就是解释了为什么二维数组可以被压缩,也解释了为什么j要从weight[i]开始;
牢记dp[j]是代表j容量的背包填满,最大价值是多少。
当容量变成3(j=weight[i])以后,dp[j]发生了什么,dp[j]有两个来源:
①不加入物品1,那就是和上一步结果一样,所以取dp[3];
②加入物品1,首先容量得腾出位置来放物品1,所以3-weight[1],也就是容量变成1了,当j=1时,dp[1]=15(最大价值是15),再加上物品1的价值value[1]=20,所以dp[3-weight[1]]+value[1]。进行最后一步,选出dp[3]和dp[3-weight[1]]+value[1]的最大值,就是当前容量填满放进去物品的最大价值。
最后,j=4或者物品2也在选择范围时,就可以用公式:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
完整代码: 注意这里先开始遍历物品再开始遍历背包容量,反过来也可以,区别于一维01背包。
void test_CompletePack() {vector<int> weight = {1, 3, 4};vector<int> value = {15, 20, 30};int bagWeight = 4;vector<int> dp(bagWeight + 1, 0);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]);}}return dp[bagWeight];
}
518. 零钱兑换 II - 力扣(LeetCode)
给你一个整数数组 coins
表示不同面额的硬币,另给一个整数 amount
表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0
。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
示例 1:
输入:amount = 5, coins = [1, 2, 5] 输出:4 解释:有四种方式可以凑成总金额: 5=5 5=2+2+1 5=2+1+1+1 5=1+1+1+1+1
示例 2:
输入:amount = 3, coins = [2] 输出:0 解释:只用面额 2 的硬币不能凑成总金额 3 。
思路:完全背包问题,amount就是背包容量,coins就是可以选择的物品。
解决:动态规划五部步曲
1.确定dp[j]的含义;
dp[j]:凑成总金额j的货币组合数为dp[j]。注意这里dp[j]求得是组合数,不是硬币的总额。
2.确定递推公式;
和494.目标和类似,dp[j]+=dp[j-coins[i]];
3.确定dp初始化;
还是和494.目标和类似,dp[0]=1;
代码随想录算法训练营第三十七天|1049. 最后一块石头的重量 II ,494. 目标和,474.一和零-CSDN博客
4.确定遍历顺序;
先遍历硬币,再遍历金额,里层遍历从前向后遍历,因为硬币可以重复用。
5.举例推导dp数组。
输入: amount = 5, coins = [1, 2, 5] ,dp状态图如下:
代码:
class Solution {
public:int change(int amount, vector<int>& coins) {vector<int> dp(amount+1,0);dp[0]=1;for(int i=0;i<coins.size();i++){for(int j=coins[i];j<=amount;j++){dp[j]+=dp[j-coins[i]];}}return dp[amount];}
};
377. 组合总和 Ⅳ - 力扣(LeetCode)
给你一个由 不同 整数组成的数组 nums
,和一个目标整数 target
。请你从 nums
中找出并返回总和为 target
的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
示例 1:
输入:nums = [1,2,3], target = 4 输出:7 解释: 所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1) 请注意,顺序不同的序列被视作不同的组合。
思路:这里nums是可以重复的,可以转换成完全背包问题。
①物品:这里物品就是nums里面元素;
②背包:就是组合,但是这里要求的是组合数量,所以dp数组里不是放价值,是放组合的数量;
拿示例1来说,就是需要背包物品总价值为4(j=4),从nums中选物品(可以重复)放入背包,有7种组合dp[j]。
解决:动态规划五步曲
1.确定dp[j]含义;
dp[j]就表示目标数为j的组合有dp[j]种。
2.确定递推公式;
和上题一样,dp[j]=dp[j]+dp[j-nums[i]]。
3.确定dp初始化;
dp[j]初始都为0,dp[0]=1。
4.确定遍历顺序;
①由于可以重复选取元素,所以这是一个完全背包问题。
②其次组合顺序不同也算一种新组合,题目要的其实是排列。
循环方式就两种:外循环物品,内循环目标数;外循环目标数,内循环物品。
如果把遍历nums(物品)放在外循环,遍历target的作为内循环的话,举一个例子:计算dp[4]的时候,结果集只有 {1,3} 这样的集合,不会有{3,1}这样的集合,因为nums遍历放在外层,3只能出现在1后面!
所以本题遍历顺序最终遍历顺序:target(背包)放在外循环,将nums(物品)放在内循环,内循环从前到后遍历。同样举个例子,当j=3时,我可以先取1再取2,或者先取2再取1,因为当前循环target没变,是通过遍历nums数组去组合。
5.举例推导dp数组。
代码:
注意:C++测试用例有两个数相加超过int的数据,所以需要在if里加上dp[i] < INT_MAX - dp[i - num]。
class Solution {
public:int combinationSum4(vector<int>& nums, int target) {vector<int> dp(target+1,0);dp[0]=1;for(int j=0;j<=target;j++){//遍历容量for(int i=0;i<nums.size();i++){//遍历物品if(j-nums[i]>=0&&dp[j] < INT_MAX - dp[j - nums[i]]){dp[j]=dp[j]+dp[j-nums[i]];}}}return dp[target];}
};
防止数组越界 要判断j-nums[i]>=0。