完全背包
有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
其他的方面几乎和01背包一样,除了遍历顺序的问题,完全背包因为可将每一个物品装入背包多次,所以遍历背包的时候不能像01背包那样倒序循环。
例如:
物品0的重量weight[0] = 1,价值value[0] = 15
如果正序遍历
dp[1] = dp[1 - weight[0]] + value[0] = 15
dp[2] = dp[2 - weight[0]] + value[0] = 30
倒序就是先算dp[2]
dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)
dp[1] = dp[1 - weight[0]] + value[0] = 15
倒序遍历能保证在计算当前值中利用的dp数组前面的值是没有将这个物品装入的值,而正序遍历则恰好会让当前计算的值所利用的前面的值已经考虑过装入这个物品了。
还有一个遍历问题是完全背包问题是否一定先遍历物品后遍历背包呢,实际上对于先遍历物品还是背包都没有关系,这是因为如果先遍历物品后遍历背包,那么在计算 j 时要用到的前面的值就是考虑过所有物品之后的值(可能已经加入过当前考虑的物品),如果直接使用(如果选择加入当前物品)就可能会导致再次使用已经用过的物品(也就是再次加入当前物品),这在01背包中是不允许的,但是多重背包本身就会多次加入当前遍历到的物品(由正序遍历背包决定),故而也就没影响了。
对于纯完全背包问题,其for循环的先后循环是可以颠倒的,但如果题目稍稍有点变化,就会体现在遍历顺序上,如果问装满背包有几种方式, 那么两个for循环的先后顺序就会有很大区别。
#include <bits/stdc++.h>
using namespace std;
int main(){
int n,bagweight;
cin>>n>>bagweight;
vector<int> weight(n);
vector<int> value(n);
for(int i=0;i<n;i++) cin>>weight[i]>>value[i];
vector<int> dp(bagweight+1,0);
for(int i=0;i<n;i++){
for(int j=weight[i];j<bagweight+1;j++){
dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
}
}
cout<<dp[bagweight]<<endl;
return 0;
}
518. 零钱兑换 II
视频讲解:动态规划之完全背包,装满背包有多少种方法?组合与排列有讲究!| LeetCode:518.零钱兑换II_哔哩哔哩_bilibili
这道题和之前的494. 目标和问题很像,就是01背包和完全背包的区别,故而除了遍历顺序外其他一样。这道题的遍历顺序很讲究,虽然说纯完全背包问题无所谓先遍历物品还是背包,但是这是因为纯完全背包不涉及到底怎样得到的最大值,实际上它包容了组合相同但排列不同的情况,但是本题求解的是组合数,所以必须分清楚遍历顺序。
首先解释一下为什么要dp数组值储存的方法数一定会是装满这个容量的方法数,这是因为每一个大的背包容量对应的方法数一定是较小的背包容量的方法数的加和(这是由递推公式决定),而对于不能分割的最小的容量,如果它不能装满,也就是遍历完全部物品后,发现全部都大于这个最小容量,那么这个容量对应的方法数就是0,而之后的容量,如果想要利用这个容量作为剩余容量来计算方法数,那么它加的也是0。
接下来解释遍历问题——
如果先遍历背包容量,那么求解容量 j 的方法数的时候可能出现同组合不同排列的情况(例如,容量6,可能遇到把{1,5}和{5,1}同时算入的情况,因遍历了全部物品,所以之前的值势必会将这个容量的全部可能情况都考虑在内,故而在填充 dp[6] 的时候,遍历到物品1时会+=dp[5],遍历到物品5时会+=dp[1],而dp[5]必然包含{5}、dp[1]必然包含{1}的情况,所以就相当于算了两遍{1,5},再装入多个物品时同理)。其实本质上就是对于一组组合数(对于最终容量的),在最后一次容量的遍历中,不断将这个组合中的不同物品放在开头,而其对应的剩下的容量dp数组值也是包含了剩下组合数的排列数,恰好符合排列数的计算方法。
如果先遍历物品,那么对于每考虑进去一个新的物品,一定能对应一种新的满足背包容量的组合数,所以不会有同组合的情况。
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];
}
在求装满背包有几种方案的时候,认清遍历顺序是非常关键的:
- 如果求组合数就是外层for循环遍历物品,内层for遍历背包。
- 如果求排列数就是外层for遍历背包,内层for循环遍历物品。
377. 组合总和 Ⅳ
视频讲解:动态规划之完全背包,装满背包有几种方法?求排列数?| LeetCode:377.组合总和IV_哔哩哔哩_bilibili
这道题就是典型的完全背包求排列数问题,直接套即可。注意C++测试用例有两个数相加超过int的数据,所以需要在if里加上dp[i] < INT_MAX - dp[i - num]。因为最后返回值一定是int类型能返回的,所以如果存在dp数组值加和之后大于INT_MAX,那就一定不可能在之后用到,所以直接跳过不相加即可。我真是对这种奇葩测试点真是无语了,什么破玩意!
int combinationSum4(vector<int>& nums, int target) {
vector<int> dp(target+1,0);
dp[0]=1;
for(int i=0;i<target+1;i++){
for(int j=0;j<nums.size();j++){
if(i>=nums[j]&&dp[i]<INT_MAX-dp[i-nums[j]]) dp[i]+=dp[i-nums[j]];
}
}
return dp[target];
}