题目链接:携带研究材料、518.零钱兑换II、377. 组合总和 Ⅳ、爬楼梯(进阶版)
文章链接:代码随想录
动态规划
1. 完全背包理论基础
有 n 件物品和一个最多能背重量为 w 的背包。第 i 件物品的重量是 weight[i],得到的价值是 value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
举例:背包最大重量为4。物品为:
重量 价值 物品0 1 15 物品1 3 20 物品2 4 30 注意:每件商品有无数个,可以无限次放入背包中。问背包能背的物品最大价值是多少?
方法一:二维 dp 数组
1. 确定 dp 数组(dp table)以及下标的含义:dp[i][j] 表示从下标为 [0-i] 的物品,每个物品可以取无限次,放进容量为 j 的背包,价值总和最大是多少
2. 推导递推公式:求取 dp[i][j] 有两种情况:放物品 i & 不放物品 i
不放物品 i:背包容量为 j,里面不放物品i的最大价值是 dp[i - 1][j]
放物品 i:背包空出物品 i 的容量后,背包容量为 j - weight[i],dp[i][j - weight[i]] 为背包容量为 j - weight[i] 且不放物品 i 的最大价值,那么 dp[i][j - weight[i]] + value[i](物品 i 的价值)就是背包放物品 i 得到的最大价值
递归公式: dp[i][j] = max ( dp[i - 1][j], dp[i][j - weight[i]] + value[i] )
注意与01背包区别:01背包是 dp[i - 1][j - weight[i]] + value[i]
3. dp 数组如何初始化:
- 首先从 dp[i][j] 的定义出发,如果背包容量 j 为 0 的话,即 dp[i][0],无论是选取哪些物品,背包价值总和一定为 0
- 再由状态转移方程 dp[i][j] = max ( dp[i - 1][j], dp[i][j - weight[i]] + value[i] ) 可以看出有一个方向 i 是由 i-1 推导出来,那么 i 为 0 的时候,也就是 dp[0][j] 一定要初始化。
for (int i = 1; i < weight.size(); i++) { // 当然这一步,如果把dp数组预先初始化为0了,这一步就可以省略,但很多同学应该没有想清楚这一点。 dp[i][0] = 0; } // 正序遍历,如果能放下就一直装物品0 for (int j = weight[0]; j <= bagWeight; j++) dp[0][j] = dp[0][j - weight[0]] + value[0];
当 j < weight[0] 时,dp[0][j] 应该是 0,因为背包容量比物品 0 重量还小
当 j > weight[0] 时,dp[0][j] 如果能放下 weight[0] 的话,就一直装,因为每一种物品有无限个
- 除了 dp[0][j] 和 dp[i][0],从递归公式可以看出,dp[i][j] 是由其上方和左上方的数值推导而来,因此,其他下标初始值可以任意设置,因为都会被覆盖
此时 dp 数组初始化情况如图所示:
4. 确定遍历顺序:对于完全背包二维 dp 数组,先遍历物品还是先遍历背包都是可以的
5. 举例推导 dp 数组:因为物品 0 的性价比是最高的,而且在完全背包中,每一类物品都有无限个,因此,既然物品 0 的性价比最高,当然是优先放物品 0
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, bagSize;
cin >> n >> bagSize;
vector<int> weight(n);
vector<int> value(n);
for(int i = 0; i < n; i++) {
cin >> weight[i] >> value[i];
}
vector<vector<int>> dp(n, vector<int>(bagSize + 1, 0));
for(int j = weight[0]; j <= bagSize; j++) {
dp[0][j] = dp[0][j - weight[0]] + value[0];
}
for(int i = 1; i < n; i++) {
for(int j = 0; j <= bagSize; j++) {
if(j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);
}
}
cout << dp[n - 1][bagSize];
}
方法二:一维 dp 数组(滚动数组)
对于递推公式:去掉 i 这个维度,完全背包以为--一维 dp 数组的递推公式变成: dp[j] = max( dp[j], dp[j - weight[i]] + value[i] )
对于遍历顺序:
已知 01 背包中,二维 dp 数组的两个 for 循环的先后顺序是可以颠倒的,一维 dp 数组的两个 for 循环的先后循序一定是先遍历物品,再遍历背包容量(且背包是倒序遍历)
而对于完全背包一维 dp 数组,两个for循环嵌套顺序是可以颠倒的,而且都是正序遍历
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, bagSize;
cin >> n >> bagSize;
vector<int> weight(n);
vector<int> value(n);
for(int i = 0; i < n; i++) {
cin >> weight[i] >> value[i];
}
vector<int> dp(bagSize + 1, 0);
for(int i = 0; i < n; i++) {
for(int j = weight[i]; j <= bagSize; j++) {
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[bagSize];
}
2. 零钱兑换II
方法一:二维数组dp[i][j]
1. 确定dp数组(dp table)以及下标的含义:dp[i][j]:使用下标为 [0, i] 的 nums[i] 能够凑满 j(包括 j)这么大容量的包,有 dp[i][j] 种方法
2. 确定递推公式:dp[i][j] = dp[i - 1][j] + dp[i][j - nums[i]]
3. dp数组如何初始化:首先考虑 dp[0][0],背包容量为 0,能装满物品 0 的方法有0 种,由于题目中不存在硬币面额为 0 的情况,因此不考虑无限个 0 的组合和为 0 的情况(与494. 目标和不同)
最上行dp[0][j](用物品 0 装满容量为 j 的背包有几种组合方法):只要 j 可以整除 coins[i],就有一种组合方法
for (int j = 0; j <= bagSize; j++) { if (j % coins[0] == 0) dp[0][j] = 1; }最左列dp[i][0](用物品 [0, i] 装满容量为 0 的背包有几种组合方法):只有一种方法,即不装,因此 dp[i][0] = 1
4. 确定遍历顺序:使用二维 dp 数组,两种遍历方式都可以(先从上到下再从左到右,先从左到右再从上到下)
5. 举例推导dp数组:以 amount 为 5,coins 为:[2,3,5] 为例:
1 0 1 0 1 0 1 0 1 1 1 1 1 0 1 1 1 2
class Solution {
public:
int change(int amount, vector<int>& coins) {
int bagSize = amount;
vector<vector<uint64_t>> dp(coins.size(), vector<uint64_t>(bagSize + 1, 0));
for(int i = 0; i < coins.size(); i++) {
dp[i][0] = 1;
}
for(int j = 0; j <= bagSize; j++) {
if(j % coins[0] == 0) dp[0][j] = 1;
}
for(int i = 1; i < coins.size(); i++) {
for(int j = 0; j <= bagSize; j++) {
if(j < coins[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]];
}
}
return dp[coins.size() - 1][bagSize];
}
};
方法二:一维数组dp[j]
1. 确定dp数组(dp table)以及下标的含义:dp[j] 表示凑满总金额为 j 的组合数为 dp[j]
2. 确定递推公式:dp[j] = dp[j] + dp[j - coins[i]] 即 dp[j] += dp[j - coins[i]]
3. dp数组如何初始化:dp[0] 初始为 1,即装满容量为 0 的背包有一种方法,放 0 件物品
4. 确定遍历顺序:对于纯完全背包使用一维 dp 数组,两种 for 循环遍历方式是可以颠倒的,而由于本题求的是组合数,两种遍历方式的结果并不相同
- 情况一:外层 物品(硬币),内层 背包容量(金额)
含义:每次固定一种硬币,计算它能对所有金额产生的影响。这样不会重复统计排列,只会统计组合for (int i = 0; i < coins.size(); i++) { // 遍历硬币 for (int j = coins[i]; j <= amount; j++) { // 遍历金额 dp[j] += dp[j - coins[i]]; } }- 情况二:外层 背包容量(金额),内层 物品(硬币)
含义:每次固定一个金额,尝试从所有硬币转移过来。这会把不同顺序的结果都算进去,等于计算排列数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]]; } }5. 举例推导dp数组:
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<uint64_t> 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];
}
};
3. 组合总和 Ⅳ
本题的思路和“518.零钱兑换II”一致,只是从求组合(不强调顺序)变成了求排列(强调顺序)
1. 确定dp数组(dp table)以及下标的含义:dp[j] 表示凑成目标正整数为 j 的排列个数为 dp[j]
2. 确定递推公式:dp[j] = dp[j] + dp[j - nums[i]] 即 dp[j] += dp[j - nums[i]]
3. dp数组如何初始化:根据递推公式 dp[j] += dp[j - nums[j]],dp[0] 要初始化为 1,这样递归其他 dp[j] 的时候才会有数值基础
4. 确定遍历顺序:对于纯完全背包使用一维 dp 数组,两种 for 循环遍历方式是可以颠倒的,而由于本题求的是排列数,两种遍历方式的结果并不相同
- 如果求组合数,就是外层 for 循环遍历物品,内层 for 循环遍历背包
- 如果求排列数,就是外层 for 循环遍历背包,内层 for 循环遍历物品
5. 举例推导dp数组:
//本题的思路和“518.零钱兑换II”一致,只是从求组合(不强调顺序)变成了求排列(强调顺序)
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
int bagSize = target;
vector<uint64_t> dp(bagSize + 1, 0); //C++测试用例有两个数相加超过int的数据
dp[0] = 1;
for(int j = 0; j <= bagSize; j++) {
for(int i = 0; i < nums.size(); i++) {
if(j >= nums[i]) dp[j] += dp[j - nums[i]];
}
}
return dp[bagSize];
}
};
4. 爬楼梯(进阶版)
之前做的爬楼梯每次至多只能爬两个台阶,进阶版改为:每次至多可以爬 m 个台阶(1 <= m < n),问有多少种不同的方法可以爬到楼顶呢?
进阶版的爬楼梯其实也是一个完全背包问题。1 阶、2 阶、.... m 阶就是物品,楼顶 n 就是背包的容量。每一阶可以重复使用,例如跳了 1 阶,还可以继续跳 1 阶。问跳到楼顶有几种方法其实就是问装满背包有几种方法。
本题和“”基本就是一道题了。
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
vector<int> dp(n + 1, 0);
dp[0] = 1;
for(int j = 0; j <= n; j++) {
for(int i = 1; i <= m; i++) {
if(j >= i) dp[j] += dp[j - i];
}
}
cout << dp[n];
}
5. 阶段总结
(待更新...)
相关题目和后续提高:




&spm=1001.2101.3001.5002&articleId=150606206&d=1&t=3&u=19ddc4a67b704b408521565423035e29)
3374

被折叠的 条评论
为什么被折叠?



