完全背包问题
完全背包与0-1背包问题非常相似,不同之处在于:
每种物品可以选择无限次,而不是只能选一次。
假设有 N 件物品和一个容量为 W 的背包。每件物品有两个属性:
体积:
v[i]价值:
w[i]
目标是从这些物品中选取若干件(每种物品可以选无限多次),使得在不超过背包容量的前提下,获得的最大总价值是多少。
完全背包的二维dp表示
1. 确定dp数组以及下标的含义
dp[i][j]:表示从下标为 [0-i] 的物品,每个物品可以取无限次,放进容量为 j 的背包,价值总和最大是多少。
2. 确定递推公式
- 不放物品
i:背包容量为j,最大价值为dp[i-1][j]。 - 放物品
i:背包空出物品i的体积v[i],剩余容量为j - v[i],dp[i][j - v[i]]表示背包容量 为j - v[i]时的最大价值。那么放入物品i后的最大价值为:dp[i][j - v[i]] + w[i]。- 放入物品
i后,背包容量为j - v[i],但是物品i还可以继续放入背包中,因此我们还是在考虑物品i。
- 放入物品
因此递推公式为:
dp[i][j] = max(dp[i-1][j], dp[i][j - v[i]] + w[i]);这里与0-1背包的递推公式不同的是:dp[i][j - v[i]] 这里的 i 没有减去1,也就是说物品 i 还可以继续放入背包中。
0 - 1背包的递推公式为:
dp[i][j] = max(dp[i-1][j], dp[i-1][j - v[i]] + w[i]);3. 初始化dp数组
首先从 dp[i][j] 的定义出发,如果背包容量 j 为0的话,即 dp[i][0],无论是选取哪些物品,背包价值总和一定为0。
状态转移方程中: dp[i][j] = max(dp[i-1][j], dp[i-1][j - v[i]] + w[i]) 可以看出,dp[i][j] 依赖于 dp[i-1][j] 和 dp[i][j - v[i]]。可以看出有一个方向 i 是由 i-1 推导出来,那么 i 为0的时候就一定要初始化。 因此我们需要初始化 dp[0][j],即当没有物品时,背包的最大价值为0。
dp[0][j],即:存放编号0的物品的时候,各个容量的背包所能存放的最大价值。
那么很明显当
j < v[0]时,dp[0][j]应该为0,因为背包容量不够放下物品0。当
j >= v[0]时,dp[0][j]应该为w[0],因为背包容量足够放下物品0。
j >= v[0],dp[0][j]如果能放下v[0]的话,就一直装,每一种物品有无限个。
因此我们可以初始化 dp[0][j] 为:
vector<int> dp(W + 1, 0);
for (int j = 0; j <= W; ++j) {
if (j >= v[0]) {
dp[j] = w[0];
}
}4. 遍历顺序
在0 - 1背包中,先遍历物品,再遍历背包容量,或者是先遍历背包容量,再遍历物品,都是可行的,因为这是由推导顺序决定的。
// 先遍历物品,再遍历背包容量
for (int i = 0; i < N; ++i) {
for (int j = 1; j <= W; ++j) {
if (j < v[i]) {
dp[i][j] = dp[i-1][j]; // 背包容量不够放下物品i
} else {
dp[i][j] = max(dp[i-1][j], dp[i][j - v[i]] + w[i]); // 放下物品i
}
}
}// 先遍历背包容量,再遍历物品
for (int j = 0; j <= W; ++j) {
for (int i = 1; i < N; ++i) {
if (j < v[i]) {
dp[i][j] = dp[i-1][j]; // 背包容量不够放下物品i
} else {
dp[i][j] = max(dp[i-1][j], dp[i][j - v[i]] + w[i]); // 放下物品i
}
}
}5. 整体代码
vector<int> knapsack(int W, vector<int>& v, vector<int>& w) {
int N = v.size();
vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0)); // dp[i][j]表示从下标为[0-i]的物品,每个物品可以取无限次,放进容量为j的背包,价值总和最大是多少。
for (int j = 0; j <= W; ++j) {
if (j >= v[0]) {
dp[0][j] = w[0];
}
}
for (int i = 1; i < N; ++i) {
for (int j = 0; j <= W; ++j) {
if (j < v[i]) {
dp[i][j] = dp[i-1][j]; // 背包容量不够放下物品i
} else {
dp[i][j] = max(dp[i-1][j], dp[i][j - v[i]] + w[i]); // 放下物品i
}
}
}
return dp[N - 1]; // 返回最后一行,即所有物品的最大价值
}完全背包的一维dp表示
1. 确定dp数组以及下标的含义
dp[j]:表示从下标为 [0-N] 的物品,每个物品可以取无限次,放进容量为 j 的背包,价值总和最大是多少。
2. 确定递推公式
在上面的二维dp表示中,dp[i][j] = max(dp[i-1][j], dp[i][j - v[i]] + w[i])。
for (int i = 0; i < N; ++i) {
for (int j = 1; j <= W; ++j) {
if (j < v[i]) {
dp[i][j] = dp[i-1][j]; // 背包容量不够放下物品i
} else {
dp[i][j] = max(dp[i-1][j], dp[i][j - v[i]] + w[i]); // 放下物品i
}
}
}将上一层 dp[i-1] 的那一层拷贝到 当前层 dp[i] ,那么 递推公式由:dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]) 变成: dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i])
所以我们可以将二维dp表示转化为一维dp表示。
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
- 这里的
j是从W到v[i]递减的,表示当前背包容量为j,如果放入物品i,那么剩余容量为j - v[i]。
3. 初始化dp数组
vector<int> dp(W + 1, 0);4. 遍历顺序
在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的,因为 dp[j] 是根据 下标 j 之前所对应的 dp[j] 计算出来的。 只要保证下标j之前的 dp[j] 都是经过计算的就可以了。
// 先遍历物品,再遍历背包容量
for (int i = 0; i < N; ++i) {
for (int j = v[i]; j <= W; ++j) { // 从v[i]开始遍历
dp[j] = max(dp[j], dp[j - v[i]] + w[i]); // 放下物品i
}
}// 先遍历背包容量,再遍历物品
for (int j = 0; j <= W; ++j) {
for (int i = 0; i < N; ++i) { // 从0开始遍历
if (j >= v[i]) {
dp[j] = max(dp[j], dp[j - v[i]] + w[i]); // 放下物品i
}
}
}零钱兑换 II
1. 状态定义
dp[i] 表示金额为 i 的组合数。
2. 状态转移方程
dp[i] = dp[i] + dp[i - coins[j]]
意思是:对于当前硬币 coin,凑成金额 j 的方法数 = 凑成金额 j - coin 的方法数之和。
dp[i]表示当前金额为i的组合数。dp[i - coins[j]]表示当前金额为i - coins[j]的组合数。
3. 初始化
dp[0] = 1,表示凑成金额为0的组合数为1(即不选任何数字)。
4. 遍历顺序
因为纯完全背包求得装满背包的最大价值是多少,和凑成总和的元素有没有顺序没关系,即:有顺序也行,没有顺序也行。
而本题要求凑成总和的组合数,元素之间明确要求没有顺序。所以纯完全背包是能凑成总和就行,不用管怎么凑的。本题是求凑出来的方案个数,且每个方案个数是组合数。
正确的遍历顺序应该是:先遍历硬币,再遍历容量(金额)
for (int coin : coins) {
for (int j = coin; j <= amount; ++j) {
dp[j] += dp[j - coin];
}
}此时先遍历硬币,就是求组合。如果先遍历的是容量,那么就会出现重复计算的情况,也就是求的是排列。
5. 整体代码
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> dp(amount + 1, 0);
dp[0] = 1; // 凑出金额为0的组合数是1(空集)
for (int coin : coins) { // 遍历每种硬币(物品)
for (int j = coin; j <= amount; ++j) { // 遍历容量,从小到大
if (dp[j] < INT_MAX - dp[j - coin]) { //防止相加数据超int
dp[j] += dp[j - coin];
}
}
}
return dp[amount];
}
};如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
组合总和 IV
因为本题的描述中,元素相同但顺序不同的组合算作不同的组合,所以这其实是一个排列问题。
所以本题的思路和零钱兑换 II 是一样的,只不过本题是求排列数。
遍历顺序是:先遍历背包,再遍历物品。
这里就直接给出代码了:
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
// 求的是排列
vector<unsigned int> dp(target + 1, 0);
dp[0] = 1; // 凑出金额为0的组合数是1(空集)
for (int i = 1; i <= target; ++i) { // 遍历容量,从小到大
for (int num : nums) { // 遍历每种硬币(物品)
if (i >= num) {
dp[i] += dp[i - num];
}
}
}
return dp[target];
}
};爬楼梯 - 完全背包问题
这里我们将爬楼梯的规则稍微改一下:
- 每次可以爬 1 ~ 无限 步。
- 也就是每次可以选择爬 1 步,2 步,3 步,…,n 步。
问题:有多少种不同的方式可以爬到楼梯的顶部?
那么这里就变成了一个完全背包问题了。
- 跳多少阶楼梯 -> 背包容量
- 爬楼梯的方式 -> 物品
| 背包问题 | 爬楼梯 |
|---|---|
| 背包容量 n | 要达到的目标楼梯高度 n |
| 物品 | 每次可以选择的步数 1, 2, …, n |
| 物品体积 | 每次爬的步数 |
| 物品价值 | 这里我们不是求最大价值,而是求组合数(方案数) |
1. 状态定义
dp[i] 表示爬到第 i 层楼梯的方式数。
2. 状态转移方程
装满背包有几种方法,递推公式一般都是 dp[i] += dp[i - nums[j]];
本题呢,dp[i] 有几种来源,dp[i - 1],dp[i - 2],dp[i - 3] 等等,即:dp[i - j]
那么递推公式为:dp[i] += dp[i - j]`
3. 初始化
dp[0] = 1,表示爬到第0层楼梯的方式数为1(即不爬)。
4. 遍历顺序
这是背包里求排列问题,即:1、2 步 和 2、1 步都是上三个台阶,但是这两种方法是不同的,所以我们需要先遍历背包,再遍历物品。
for (int i = 1; i <= n; ++i) { // 遍历背包
for (int j = 1; j <= m; ++j) { // 遍历物品
if (i >= j) {
dp[i] += dp[i - j];
}
}
}5. 整体代码
class Solution {
public:
int climbStairs(int n) {
vector<int> dp(n + 1, 0);
dp[0] = 1; // 爬到第0层楼梯的方式数为1(即不爬)
for (int i = 1; i <= n; ++i) { // 遍历背包
for (int j = 1; j <= i; ++j) { // 遍历物品
if (i >= j) {
dp[i] += dp[i - j];
}
}
}
return dp[n];
}
};
