目录
一、做题心得
今天是代码随想录打卡的第37天,来到了动态规划章节的完全背包问题。完全背包问题相较于前边打卡的01背包问题,由一个物品只能使用一次,变成了一个物品可以使用多次。那么代码上自然也会存在相应改动。不过由于都属于背包问题的范畴,思路上讲是一致的。因此,有了前边打卡的基础,今天的内容就比较简单了。
话不多说,直接开始今天的内容。
二、完全背包模板
模板题:AcWing 3. 完全背包问题
题目链接
3. 完全背包问题 - AcWing题库
模板1:最大价值问题
#include<bits/stdc++.h>
using namespace std;
const int N = 1005;
int n, m;
int v[N];
int w[N];
int dp[N] = {0}; //也可以vector数组
signed main() {
cin>>n>>m;
for (int i = 0; i < n; i++) {
cin>>v[i]>>w[i];
}
for (int i = 0; i < n; i++) { //遍历物品
for (int j = 0; j <= m; j++) { //注意:正序遍历背包(从0到最大容量)--表示物品可以重复使用--注意与01背包区别
if (j >= v[i]) { //当前背包能装下该物品时(注意是大于等于)
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
}
}
cout<<dp[m];
return 0;
}
其实完全背包和01背包问题的主要区别就在于:在遍历背包上,01背包采用逆序遍历,而完全背包采用顺序(正序)遍历--这是因为逆序遍历确定了每个物品只能选一次,而顺序遍历时每个物品可以选择多次。还有一个区别就是01背包只能先遍历物品再遍历背包,而完全背包两种顺序都可以。
还有一道模板题和这个一致:52. 携带研究材料(第七期模拟笔试) (kamacoder.com)
附一份代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 10005;
int n, m;
int v[N];
int w[N];
int dp[N] = {0};
signed main() {
cin>>n>>m;
for (int i = 0; i < n; i++) {
cin>>v[i]>>w[i];
}
for (int i = 0; i < n; i++) {
for (int j = 0; j <= m; j++) {
if (j >= v[i]) {
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
}
}
cout<<dp[m];
return 0;
}
当然,这个模板是为了解决完全背包问题对于求得最大价值这一类问题,后边还会提到解决求得方案数(组合数或者排列数)这一类完全背包问题。
模板2:组合排列数问题
组合数:同一组合,不同顺序,一种情况
排列数:同一组合,不同顺序,不同情况
组合数
先遍历物品,再遍历背包
for (int i = 0; i < n; i++) { //组合数--先遍历物品,再遍历背包
for (int j = 0; j <= m; j++) {
if (j >= v[i]) { //注意:>=
dp[j] += dp[j - v[i]];
}
}
}
排列数
先遍历背包,再遍历物品
for (int i = 0; i <= m; i++) { //排列数--先遍历背包,再遍历物品
for (int j = 0; j < n; j++) {
if (i >= v[j]) { //注意:>=
dp[i] += dp[i - v[j]];
}
}
}
三、题目与题解
题目一:518. 零钱兑换 II
题目链接
518. 零钱兑换 II - 力扣(LeetCode)
题解:动态规划--完全背包
这道题一看到每一种面额的硬币有无限个,就可以联想到这是一个完全背包的组合数问题。
题目要求得出可以凑成总金额的硬币组合数,其实就是刚刚模板2中的组合数问题。(不讲求顺序)在这里物品就是硬币,dp[j]表示凑成金额总数为j的组合数。
这里需要注意的就是初始化 dp[0] = 1。
代码如下:
class Solution {
public:
int change(int amount, vector<int>& coins) {
/* 完全背包问题--一维dp */
vector<int> dp(5001, 0); //dp[j]:凑成金额总数为j的组合数
dp[0] = 1; //注意:初始化dp[0] = 1(后续中,若j == coins[i],选择当前硬币,就出现dp[0],若dp[0] == 0,组合数就没有增加)
for (int i = 0; i < coins.size(); i++) { //组合数--先遍历物品,再遍历背包
for (int j = 0; j <= amount; j++) {
if (j >= coins[i]) { //注意:>=
dp[j] += dp[j - coins[i]];
}
}
}
return dp[amount];
}
};
题目二:377. 组合总和 Ⅳ
题目链接
377. 组合总和 Ⅳ - 力扣(LeetCode)
题解:动态规划--完全背包
这道题就是一个典型的完全背包的排列数问题,即每个组合的情况是需要讲求顺序的。
在这里需要注意的就是需要防溢出:为了防止dp[i] + dp[i - nums[j]] 过大溢出,我们需要保证 dp[i - nums[j]] < INT_MAX - dp[i]。
其余的思路上就跟上一题一致,只是排列数问题需要先遍历背包,再遍历物品。
代码如下:
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<int> dp(1005, 0);
dp[0] = 1; //注意求方案数的背包问题:初始化dp[0] = 1
for (int i = 0; i <= target; i++) { //排列数--先遍历背包再遍历物品
for (int j = 0; j < nums.size(); j++) {
if (i >= nums[j] && dp[i - nums[j]] < INT_MAX - dp[i]) { //题目数据保证答案符合 32 位整数范围:dp[i - nums[j]] < INT_MAX - dp[i]在这里用作防溢出
dp[i] += dp[i - nums[j]];
}
}
}
return dp[target];
}
};
题目三:57. 爬楼梯(卡码网)
题目链接
57. 爬楼梯(第八期模拟笔试) (kamacoder.com)
题解:动态规划--完全背包
这题也比较简单,很容易想到完全背包的做法--完全背包的排列数问题(即需要讲求顺序)。
物品在这就是指你能爬台阶的步数(1--m相当于各个物品的体积),背包就是台阶。dp[i]表示台阶数为 i 时爬到楼顶的方案数(其实就是排列数)。
理清题意之后,这里也是直接套用模板2的排列数问题。
代码如下:
#include<bits/stdc++.h>
using namespace std;
int n, m;
vector<int> dp(35, 0);
signed main() {
cin>>n>>m;
dp[0] = 1;
for(int i = 0; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (i >= j) {
dp[i] += dp[i - j];
}
}
}
cout<<dp[n];
return 0;
}
三、小结
有了前边01背包的打卡练习,今天完全背包的问题也变得容易理解。理解了模板的由来之后,今天的打卡基本上就是模板的套用。最后,今天的打卡到此结束,后边将会继续加油!