目录
[LeetCode:518. 零钱兑换 II](#LeetCode:518. 零钱兑换 II)
[LeetCode:377. 组合总和 Ⅳ](#LeetCode:377. 组合总和 Ⅳ)
[卡码网:70. 爬楼梯 (进阶)](#卡码网:70. 爬楼梯 (进阶))
完全背包理论基础
文字讲解:卡码网:52. 携带研究材料
完全背包核心思想
有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次) ,求解将哪些物品装入背包里物品价值总和最大。完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
举个例子进行说明:
|-----|----|----|
| | 重量 | 价值 |
| 物品0 | 1 | 15 |
| 物品1 | 3 | 20 |
| 物品2 | 4 | 30 |
其中,背包最大重量为4,并且每件商品都有无限个!
01背包和完全背包唯一不同就是体现在遍历顺序上,我们知道01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。而完全背包的物品是可以添加多次的,所以要从小到大去遍历。
// 先遍历物品,再遍历背包
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]);
}
}
两层遍历顺序
在01背包中其实提到过,01背包中二维dp数组的两个for遍历的先后循序是可以颠倒了,一维dp数组的两个for循环先后循序一定是先遍历物品,再遍历背包容量。而在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!
那么我们到底应该先遍历物品还是先遍历背包呢? 那么就需要看题目要求的到底是组合还是排序相关的问题了。组合不强调元素之间的顺序,排列强调元素之间的顺序!
C++代码
#include <iostream>
#include <vector>
using namespace std;
// 先遍历背包,再遍历物品
void test_CompletePack(vector<int> weight, vector<int> value, int bagWeight) {
vector<int> dp(bagWeight + 1, 0);
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
for(int i = 0; i < weight.size(); i++) { // 遍历物品
if (j - weight[i] >= 0) dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[bagWeight] << endl;
}
int main() {
int N, V;
cin >> N >> V;
vector<int> weight;
vector<int> value;
for (int i = 0; i < N; i++) {
int w;
int v;
cin >> w >> v;
weight.push_back(w);
value.push_back(v);
}
test_CompletePack(weight, value, V);
return 0;
}
LeetCode:518. 零钱兑换 II
基本思路
对于这个题目,我们可以看出是求组合而不是求排序,因此不需要考虑元素之前的顺序。
- 确定dp数组以及下标的含义
dp[j]:将不同面额的硬币放满容量为j的背包中的组合数为dp[j]。
- 确定递推公式
dp[j] 就是所有的dp[j - coins[i]](考虑coins[i]的情况)相加。所以递推公式:dp[j] += dp[j - coins[i]]; 并且递推公式之前便在讲解01背包中的494.目标和提到过。
- dp数组如何初始化
首先dp[0]一定要为1,dp[0] = 1是 递归公式的基础。可以理解为不放任何硬币就放满容量为0的背包有1种方法。
- 确定遍历顺序
本题中我们是外层for循环遍历物品(钱币),内层for遍历背包(金钱总额),还是外层for遍历背包(金钱总额),内层for循环遍历物品(钱币)呢?而本题要求凑成总和的组合数,元素之间明确要求没有顺序。因此应该先遍历物品,在遍历背包。
假设:coins[0] = 1,coins[1] = 5。**如果我们先遍历背包,再遍历物品会出现什么情况呢?**在外层for循环遍历一次的过程中,会遍历多次物品,这样就不仅会出现{1, 5}这种情况,还会出现{5, 1}的情况。
- 举例推导dp数组
输入: amount = 5, coins = [1, 2, 5] ,dp状态图如下:
最后红色框dp[amount]为最终结果。
C++代码
注意:另外在递推公式中提到dp[j] += dp[j - coins[i]],而为了防止相加的数据超过int数据范围,因此要在递推公式前面加上dp[j] < INT_MAX - dp[j-coins[i]]。
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> dp(amount + 1, 0);
dp[0] = 1; // 只有一种方式达到0
for (int i = 0; i < coins.size(); i++) { // 遍历物品
for (int j = coins[i]; j <= amount; j++) { // 遍历背包
if (dp[j] < INT_MAX - dp[j - coins[i]]) { //防止相加数据超int
dp[j] += dp[j - coins[i]];
}
}
}
return dp[amount]; // 返回组合数
}
};
LeetCode:377. 组合总和 Ⅳ
文字讲解:LeetCode:377. 组合总和 Ⅳ
基本思路
根据例一种的提示,显然存在由于元素排列不同而导致结果不同的情况,因此这属于排列问题,需要考虑元素排列的顺序。
- 确定dp数组以及下标的含义
dp[i]: 从整数数组nums中选择任意整数(可以多次选择同一整数),使其相加和等于target的组个个数为dp[i]个。
- 确定递推公式
dp[i](考虑nums[j])可以由 dp[i - nums[j]](不考虑nums[j])推导出来。因为只要得到nums[j],排列个数dp[i - nums[j]],就是dp[i]的一部分。
和前面提到的几个题目类似,求装满背包有几种方法,递推公式一般都是dp[i] += dp[i - nums[j]];
- dp数组如何初始化
dp[0]要初始化为1,这样递归其他dp[i]的时候才会有数值基础。因为题目提到target是正整数,因此实际上dp[0]是没有意义的,仅仅是为了后续递推公式的推导。
- 确定遍历顺序
个数可以不限使用,说明这是一个完全背包。而得到的集合是排列,说明需要考虑元素之间的顺序。因此我们需要先遍历背包容量,再遍历物品。即外层for循环遍历target,内层for循环遍历nums。
- 举例来推导dp数组
推导出的dp状态图如下:
C++代码
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<int> dp(target + 1, 0);
dp[0] = 1;
for (int i = 0; i <= target; i++) { // 遍历背包
for (int j = 0; j < nums.size(); j++) { // 遍历物品
if (i - nums[j] >= 0 && dp[i] < INT_MAX - dp[i - nums[j]]) {
dp[i] += dp[i - nums[j]];
}
}
}
return dp[target];
}
};
卡码网:70. 爬楼梯 (进阶)
文字讲解:卡码网:70. 爬楼梯 (进阶)
基本思路
看到这个题目和容易想到前面01背包时候做过的爬楼梯的题目。而之前做的题目最多只能爬2个台阶,但是如果能够最多爬m个台阶呢?其实这个问题同样可以映射为完全背包的问题,即将允许一次最多爬m个台阶看做物品,而台阶总数看作是背包容量。(这里就不在用动规五部曲进行解释了,其实和前面的完全背包是类似的)。
另外需要明确的一点是,爬台阶可以看作是排列问题而并非组合问题,这一点很重要!
C++代码
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, m;
while (cin >> n >> m) {
vector<int> dp(n + 1, 0);
dp[0] = 1;
for (int i = 1; i <= n; i++) { // 遍历背包
for (int j = 1; j <= m; j++) { // 遍历物品
if (i - j >= 0) dp[i] += dp[i - j];
}
}
cout << dp[n] << endl;
}
}