一、完全背包
题目链接/文章讲解/视频讲解:https://programmercarl.com/%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80%E5%AE%8C%E5%85%A8%E8%83%8C%E5%8C%85.html
状态:已解决
1.问题介绍
完全背包的模板题目:有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
它与01背包的唯一区别在于物品的数量(01背包一种物品只有一个,而完全背包有无限个)。
2.解法
因为完全背包只有物品数量不一样,其余都是与01背包一致的,故dp数组、递推公式都是不变的,要变的只有遍历顺序。在01背包中,我们内循环是从大到小去遍历的,母的是保证每个物品仅被添加一次。而完全背包物品是可以被添加多次的,也就是说,要从小到大去遍历。具体原因在动态规划:关于01背包问题,你该了解这些!(滚动数组)中也说过了,从小到大遍历代表第 i 个物品可以一直被添加到dp数组中。如图:
这里还有一个问题: 为什么遍历物品在外层循环,遍历背包容量在内层循环?其实两种顺序都可以,因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。在一维背包-CSDN博客中,我说过了,因为一维dp的写法,背包容量一定是要倒序遍历,如果遍历背包容量放在上一层,那就是先计算最后一列的数值,而我们知道某个格子的数值是依赖左上角的值的,但此时左上角(倒数第二列)还没计算,故推不出正确结果,因此只能是物品先遍历。也就是倒序遍历决定了我们只能先遍历物品。而此题并不需要倒序遍历,因此内外层哪种遍历顺序都能使得左上角和正上方的格子先被计算了。
遍历物品在外层循环,遍历背包容量在内层循环,状态如图:
遍历背包容量在外层循环,遍历物品在内层循环,状态如图:
3.代码实现
#include<bits/stdc++.h>
using namespace std;
int main(void){
int N,V;
cin>>N>>V;
vector<int> w(N);
vector<int> v(N);
for(int i=0;i<N;i++){
cin>>w[i]>>v[i];
}
vector<int> dp(V+1,0);
for(int i=0;i<N;i++){
for(int j=w[i];j<=V;j++){
dp[j] = max(dp[j],dp[j-w[i]]+v[i]);
}
}
cout<<dp[V];
}
二、518. 零钱兑换 II
题目链接/文章讲解/视频讲解:https://programmercarl.com/0518.%E9%9B%B6%E9%92%B1%E5%85%91%E6%8D%A2II.html
状态:已解决
1.思路
这道题就是典型的背包问题,且题目说了钱币的数量不限,故是背包问题中的完全背包。但此题跟纯完全背包又不一样,纯完全背包是凑成背包最大价值是多少,而本题是要求凑成总金额的物品组合个数!求的是组合数!
我们用动规五部曲来分析此题:
(1)确定dp数组以及下标含义:
没有多余的维度,一维数组dp就够了。dp[j]:凑成总金额为 j 的货币组合数为dp[j]。
(2)确定递推公式:
dp[j]就是所有dp[j - coins[i]]的值相加。
所以递推公式为:dp[j] += dp[j - coins[i]]。
(3)dp数组初始化:
首先dp[0]一定要为1,dp[0] = 1是 递归公式的基础。如果dp[0] = 0 的话,后面所有推导出来的值都是0了。其含义是如果正好选了coins[i]后,也就是j-coins[i] == 0的情况表示这个硬币刚好能选,此时dp[0]为1表示只选coins[i]存在这样的一种选法。
(4)确定遍历顺序:
本题中我们是外层for循环遍历物品(钱币),内层for遍历背包(金钱总额),还是外层for遍历背包(金钱总额),内层for循环遍历物品(钱币)呢?
我在上面的完全背包理论中讲了完全背包的两个for循环的先后顺序都是可以的。但本题就不行了!
因为纯完全背包求得装满背包的最大价值是多少,和凑成总和的元素有没有顺序没关系,即:有顺序也行,没有顺序也行!也就是说,在纯完全背包问题中,我们只关心最后的背包的总价值,不关心物品放进去的顺序。
而本题要求凑成总和的组合数,元素之间明确要求没有顺序。那么本题,两个for循环的先后顺序可就有说法了。
(1)我们先来看 外层for循环遍历物品(钱币),内层for遍历背包(金钱总额)的情况。
for (int i = 0; i < coins.size(); i++) { // 遍历物品 for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量 dp[j] += dp[j - coins[i]]; } }
假设:coins[0] = 1,coins[1] = 5。那么就是先把1加入计算,然后再把5加入计算,得到的方法数量只有{1, 5}这种情况。而不会出现{5, 1}的情况。所以这种遍历顺序中dp[j]里计算的是组合数!
(2)外层for循环遍历背包(金钱总额),内层for遍历物品(钱币)的情况。
for (int j = 0; j <= amount; j++) { // 遍历背包容量 for (int i = 0; i < coins.size(); i++) { // 遍历物品 if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]]; } }
背包容量的每一个值,都是经过 1 和 5 的计算,包含了{1, 5}(j = 6, coins[i] = 1) 和 {5, 1}(j = 6, coins[i] = 5)两种情况。
此时dp[j]里算出来的就是排列数!
排列数多于组合数,故不可以这样遍历。
那为什么在纯完全背包问题中就行呢?因为对于{1,5}和{5,1},它们放进去加起来背包得到的价值都是6,故效果是相等的。
(5) 举例推导dp数组:
2.代码实现
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]];
}
}
//for(int i=0;i<=amount;i++) cout<<dp[i]<<" ";
return dp[amount];
}
};
时间复杂度:O(mn)
空间复杂度:O(m)
三、377. 组合总和 Ⅳ
题目链接/文章讲解/视频讲解:https://programmercarl.com/0377.%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8C%E2%85%A3.html
状态:已解决
1.思路
这题明面上是求组合,实际就是求排列。回溯是求排列问题的好手,但此题不需要打印集合,只需要求个数,因此还用不着回溯,动规足矣!
这道题也是简单的完全背包问题,跟上一道题的区别无非就是一个求组合一个求排列,而我们在上道题说了组合和排列的区别实际就是遍历顺序的不同。组合的遍历顺序是先物品再容量,排序的遍历顺序是先容量再物品。因此,我们只需要在上一道题的基础上修改一下遍历顺序即可。
2.代码实现
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])
dp[j] += dp[j-nums[i]];
}
}
return dp[target];
}
};
时间复杂度:O(mn) m为target的值,n为nums的长度
空间复杂度:O(m)