代码随想录算法训练营
—day37
前言
今天是算法营的第37天,希望自己能够坚持下来!
今日任务:
● 完全背包
● 518. 零钱兑换 II
● 377. 组合总和 Ⅳ
● 70. 爬楼梯 (进阶)
一、完全背包
完全背包:
跟01背包的区别就是完全背包的物品是可以无限次使用的。
二维dp数组
思路:
-
dp[i][j]的定义为:跟01背包一样,表示从下标为[0-i]的物品,每个物品可以取无限次,放进容量为j的背包,价值总和最大是多少。
-
递归公式:dp[i][j] = max(dp[i-1][j], dp[i][j-weight(i)] + vaule[i])
①在01背包问题中,物品只能放一次,所以如果选定物品i,那么组合的个数就取决于i以前的物品的组合数:dp[i-1]
②而完全背包问题里,物品可以重复放,对于一个物品i来说,即使是选定了一个物品i,因为仍然可能放入重复的物品i,所以取决于i和i以前的物品的组合数:dp[i] -
初始化:由递推公式可知,需要初始化最左列和最上行。
①背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0,最左列初始化为0。
②最上行时放物品0,因为可以放无限次,那么就有当j >= weight[0]时,dp[0][j] 如果能放下weight[0]的话,就一直装。
for (int j = weight[0]; j <= bagWeight; j++) //空间充足的话,一直累加value[0]
dp[0][j] = dp[0][j - weight[0]] + value[0]; -
遍历顺序:对于二维dp数组来说,递推公式所需要的值,二维dp数组里对应的位置都有。所以先遍历物品还是背包都可以。
-
举例推导dp数组:
代码如下:
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, bagWeight;
int w, v;
cin >> n >> bagWeight;
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>(bagWeight + 1, 0));
// 初始化
for (int j = weight[0]; j <= bagWeight; j++)
dp[0][j] = dp[0][j - weight[0]] + value[0];
for (int i = 1; i < n; i++) { // 遍历物品
for(int j = 0; j <= bagWeight; 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][bagWeight] << endl;
return 0;
}
一维dp数组
- dp[j]的定义为:跟01背包一样,背包容量为j,能装最大价值dp[j]的物品
- 递归公式:完全背包和01背包的递归公式区别在于dp[i],但因为一维dp数组没有i维度,所以完全背包和01背包的一维dp数组是一样的,dp[j] = dp[j - weight[i]] + vaule[i]
- 初始化:初始化成0就可以了。
- 遍历顺序:在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的。因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。
代码如下:
#include<iostream>
#include<vector>
using namespace std;
int main() {
int n,v;
cin >> n >> v;
vector<int> weight(n);
vector<int> value(n);
for (int i = 0; i < n; i++) {
cin >> weight[i] >> value[i];
}
vector<int>dp (v + 1, 0);
//这里先遍历物品或者背包都可以
for (int i = 0; i < n; i++) { //遍历物品
for (int j = weight[i]; j <= v; j++) { //遍历背包,因为物品可以重复选择,所以从头开始遍历
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[v] << endl;
return 0;
}
二、518.零钱兑换II
二维dp数组
思路:
本题求的是装满这个背包的物品组合数是多少。因为每一种面额的硬币有无限个,所以这是完全背包。但又不是纯完全背包。纯完全背包是求最大价值是多少,这道题是求组合数是多少。
- dp[i][j]的定义为:用硬币i凑出金额j的组合有dp[i][j]种
- 递归公式:dp[i][j] = dp[i-1][j] + dp[i][j-coins[i]] 不放i+放i (预留coins[i]的位置,但因为可以重复放,仍有dp[i]种组合) ,如果j-coins[i] 小于0,就是放不下,那就等于不放i,dp[i-1][j]
- 初始化:dp[0][j]:当j能整除硬币i时,说明可以凑出j,有1种组合,否则就是0。
dp[i][0], 用物品i(即coins[i]) 装满容量为0的背包,都有一种方法,即不装。
所以 dp[i][0] 都初始化为1
需要初始化最左列和最上行:
- 遍历顺序:二维DP数组的完全背包的两个for循环先后顺序是无所谓的。先遍历背包还是先遍历物品都可以。
- 打印DP数组:
以amount为5,coins为:[2,3,5] 为例:
dp数组应该是这样的:
1 0 1 0 1 0
1 0 1 1 1 1
1 0 1 1 1 2
代码如下:
class Solution {
public:
//二维dp
//dp[i][j]:用硬币i凑出金额j的组合有dp[i][j]种
//dp[i][j] = dp[i-1][j] + dp[i][j-coins[i]] 不放i+放i(预留coins[i]的位置,但因为可以重复放,仍有dp[i]种组合)
//如果j-coins[i] 小于0,就是放不下,那就等于不放i,dp[i-1][j]
//初始化 dp[0][j]:当j能整除硬币i时,说明可以凑出j,有1种组合,否则就是0
//dp[i][0]:要凑出0,就是全都不放,所以是1种组合
//遍历顺序 先遍历物品再比遍历背包
int change(int amount, vector<int>& coins) {
//这里用int的话leetcode数据会溢出,需要使用uint64_t 64位无符号整数类型
//补充:unsigned long long在大多数平台上也表示64位无符号整数,但会根据平台的不同而有差异
//而uint64_t是所有平台通用的64位无符号整数类型
vector<vector<uint64_t>> dp (coins.size(), vector<uint64_t>(amount+1, 0));
//初始化最左列
for (int i = 0; i < coins.size(); i++) {
dp[i][0] = 1;
}
//初始化最上行
for (int j = 0; j <= amount; j++) {
if (j % coins[0] == 0) dp[0][j] = 1;
}
//以下遍历顺序行列可以颠倒
for (int i = 1; i < coins.size(); i++) { //行,遍历物品,从物品1开始,物品0已经初始化了
for (int j = 0; j <= amount; 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][amount];
}
};
一维dp数组
- dp[j]:凑成总金额j的货币组合数为dp[j]
- 递归公式:dp[j] = dp[j] +dp[j-coins[i]] 不放i+放i (预留coins[i]的位置) ,因为是求组合数,所以两种情况的组合数相加。
- 初始化:dp[0]: 装满容量为0的背包,都有一种方法,即不装。dp[0] = 1;
- 遍历顺序:要求组合数,需要先遍历物品再比遍历背包(正序)。
在纯完全背包中,是可以调换物品和背包的遍历顺序的,因为求的是最大金额,对凑成的元素顺序没有要求。
如果先遍历背包,再遍历物品,是先固定的背包容量,然后物品一个个放进去,这样的话就会出现{a,b}{b,a}两种集合,如下图所示:
遍历背包j=1时,记录了dp[1] = 1 ->集合{1}
遍历背包j=2时,记录了dp[2] = 2 ->集合{2}和{1,1}两种方法
遍历背包j=3时,遍历硬币1,先选定了硬币1,容量还剩2,所以是dp[2]种方法,也就是将1加入到dp[2]的两个集合中,得到集合{1,2}和{1,1,1}
遍历硬币2,先选定了硬币2,容量还剩1,所以是dp[1]种方法,将2加入到dp[1]的集合中,得到集合{2,1}
此时就得到了{1,2}和{2,1},不符合求组合的要求
代码如下:
class Solution {
public:
//一维dp:
//dp[j]含义:凑满金额为j的硬币组合数位dp[j]
//递推公式 dp[j] = dp[j] + dp[j-coins[i]] 就是二维dp把dp[i]维度去掉
//初始化:dp[0] = 1 凑满金额0的方法是1,全都不放
//遍历顺序,只能先遍历物品再遍历背包,求的是组合,如果反过来的话会出现[物品1,物品2],[物品2,物品1]这种组合
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++) { //遍历物品,从物品0开始
for (int j = coins[i]; j <= amount; j++) { //遍历背包,只需要从能放下物品i的背包开始
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
};
三、377. 组合总和 Ⅳ
思路:
本题就是完全背包求排列。思路是跟上一题的518.零钱兑换II一样,只是遍历顺序需要改变。
- dp[i]: 凑成目标正整数为i的排列个数为dp[j]
- 递归公式:dp[i] = dp[i] + dp[i - nums[j]] (放数字i和不放数字i的组合数相加)
- 初始化:因为递推公式dp[i] += dp[i - nums[j]]的缘故,dp[0]要初始化为1,这样递归其他dp[i]的时候才会有数值基础。
- 遍历顺序:因为求排列数,所以是外层for遍历背包,内层for循环遍历物品。
- 举例推导dp数组:
代码如下:
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<uint64_t> dp(target+1, 0);
dp[0] = 1;
//要求的排列,需要先遍历背包
for (int j = 1; j <= target; j++) { //遍历背包
for (int i = 0; i < nums.size(); i++) { //遍历物品
if (j < nums[i]) continue;
dp[j] += dp[j - nums[i]];
}
}
return dp[target];
}
};
70. 爬楼梯(进阶版)
思路:
可以抽象成完全背包,台阶是背包,每一次爬梯就是选择不同的物品,爬的阶数是物品的价值,也是重量。
- dp[i]:爬到有i个台阶的楼顶,有dp[i]种方法。
- 递归公式:dp[i]有几种来源,dp[i - 1],dp[i - 2],dp[i - 3] 等等,即:dp[i - j],
所以dp[i] += dp[i - j] - 初始化:递归公式是 dp[i] += dp[i - j],那么dp[0] 一定为1,dp[0]是递归中一切数值的基础所在,如果dp[0]是0的话,其他数值都是0了。
- 遍历顺序:这是背包里求排列问题,即:1、2 步 和 2、1 步都是上三个台阶,但是这两种方法不一样。所以先遍历背包,再遍历物品
代码如下:
#include<iostream>
#include<vector>
using namespace std;
int main() {
//dp[j]:j个台阶,有dp[j]种方法可以爬到楼顶
//台阶是背包,每一次爬梯就是选择不同的物品,爬的阶数是物品的价值
int m, n; //m:最多可以走m步,有n个台阶
cin >> n >> m;
vector<int> dp(n+1, 0);
dp[0] = 1;
//遍历顺序,因为走台阶是有先后顺序的,需要求排列
for (int j = 1; j <= n; j++) { //遍历背包
for (int i = 1; i <= m; i++) { //遍历物品
if (j >= i)
dp[j] += dp[j-i];
}
}
cout << dp[n] << endl;
return 0;
}
总结
01背包求最大价值:
- 二维数组:递推公式dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]),
先遍历物品还是背包都可以。
一维数组:递推公式dp[j] = max(dp[j], dp[j-weight[i]] + value[i]),
必须先遍历物品,然后背包后序遍历。
纯完全背包,求装满背包的价值:
- 二维数组:递推公式dp[i][j] = max(dp[i-1][j], dp[i][j-weight[i]] + value[i]),
先遍历物品还是背包都可以。 - 一维数组:递推公式dp[j] = max(dp[j], dp[j-weight[i]] + value[i]),
先遍历物品还是背包都可以。
完全背包,求组合数:
- 1.二维数组:递推公式dp[i][j] = dp[i-1][j] + dp[i][j - weight[i]],
先遍历物品还是背包都可以。 - 一维数组:递推公式dp[j] += dp[j - weight[i]],
必须先遍历物品,背包正序遍历。(如果求的是排列数的话,需要先遍历背包)
明天继续加油!