Files
leetcode-master/problems/背包问题理论基础完全背包.md
2024-12-11 11:55:32 +08:00

325 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<p align="center">
<a href="https://www.programmercarl.com/xunlian/xunlianying.html" target="_blank">
<img src="../pics/训练营.png" width="1000"/>
</a>
<p align="center"><strong><a href="./qita/join.md">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!</strong></p>
# 完全背包理论基础-二维DP数组
本题力扣上没有原题,大家可以去[卡码网第52题](https://kamacoder.com/problempage.php?pid=1052)去练习,题意是一样的。
## 完全背包
有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i]得到的价值是value[i] 。**每件物品都有无限个(也就是可以放入背包多次)**,求解将哪些物品装入背包里物品价值总和最大。
**完全背包和01背包问题唯一不同的地方就是每种物品有无限件**
同样leetcode上没有纯完全背包问题都是需要完全背包的各种应用需要转化成完全背包问题所以我这里还是以纯完全背包问题进行讲解理论和原理。
在下面的讲解中,我拿下面数据举例子:
背包最大重量为4物品为
| | 重量 | 价值 |
| ----- | ---- | ---- |
| 物品0 | 1 | 15 |
| 物品1 | 3 | 20 |
| 物品2 | 4 | 30 |
**每件商品都有无限个!**
问背包能背的物品最大价值是多少?
**如果没看到之前的01背包讲解已经要先仔细看如下两篇01背包是基础本篇在讲解完全背包之前的背包基础我将不会重复讲解**
* [01背包理论基础二维数组](https://programmercarl.com/背包理论基础01背包-1.html)
* [01背包理论基础一维数组](https://programmercarl.com/背包理论基础01背包-2.html)
动规五部曲分析完全背包为了从原理上讲清楚我们先从二维dp数组分析
### 1. 确定dp数组以及下标的含义
**dp[i][j] 表示从下标为[0-i]的物品每个物品可以取无限次放进容量为j的背包价值总和最大是多少**
很多录友也会疑惑,凭什么上来就定义 dp数组思考过程是什么样的 这个思考过程我在 [01背包理论基础二维数组](https://programmercarl.com/背包理论基础01背包-1.html) 中的 “确定dp数组以及下标的含义” 有详细讲解。
### 2. 确定递推公式
这里在把基本信息给出来:
| | 重量 | 价值 |
| ----- | ---- | ---- |
| 物品0 | 1 | 15 |
| 物品1 | 3 | 20 |
| 物品2 | 4 | 30 |
对于递推公式,首先我们要明确有哪些方向可以推导出 dp[i][j]。
这里依然拿dp[1][4]的状态来举例: [01背包理论基础二维数组](https://programmercarl.com/背包理论基础01背包-1.html) 中也是这个例子,要注意下面的不同之处)
求取 dp[1][4] 有两种情况:
1. 放物品1
2. 还是不放物品1
如果不放物品1 那么背包的价值应该是 dp[0][4] 即 容量为4的背包只放物品0的情况。
推导方向如图:
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20241126112952.png)
如果放物品1 **那么背包要先留出物品1的容量**目前容量是4物品1 的容量就是物品1的重量为3此时背包剩下容量为1。
容量为1只考虑放物品0 和物品1 的最大价值是 dp[1][1] **注意 这里和 [01背包理论基础二维数组](https://programmercarl.com/背包理论基础01背包-1.html) 有所不同了**
在 [01背包理论基础二维数组](https://programmercarl.com/背包理论基础01背包-1.html) 中背包先空留出物品1的容量此时容量为1只考虑放物品0的最大价值是 dp[0][1]**因为01背包每个物品只有一个既然空出物品1那背包中也不会再有物品1**
而在完全背包中,物品是可以放无限个,所以 即使空出物品1空间重量那背包中也可能还有物品1所以此时我们依然考虑放 物品0 和 物品1 的最大价值即: **dp[1][1] 而不是 dp[0][1]**
所以 放物品1 的情况 = dp[1][1] + 物品1 的价值,推导方向如图:
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20241126113104.png)
**注意上图和 [01背包理论基础二维数组](https://programmercarl.com/背包理论基础01背包-1.html) 中的区别**,对于理解完全背包很重要)
两种情况分别是放物品1 和 不放物品1我们要取最大值毕竟求的是最大价值
`dp[1][4] = max(dp[0][4], dp[1][1] + 物品1 的价值) `
以上过程,抽象化如下:
* **不放物品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]);`
注意完全背包二维dp数组 和 01背包二维dp数组 递推公式的区别01背包中是 `dp[i - 1][j - weight[i]] + value[i])`
### 3. dp数组如何初始化
**关于初始化一定要和dp数组的定义吻合否则到递推公式的时候就会越来越乱**
首先从dp[i][j]的定义出发如果背包容量j为0的话即dp[i][0]无论是选取哪些物品背包价值总和一定为0。如图
![动态规划-背包问题2](https://code-thinking-1253855093.file.myqcloud.com/pics/2021011010304192.png)
在看其他情况。
状态转移方程 `dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);` 可以看出有一个方向 i 是由 i-1 推导出来那么i为0的时候就一定要初始化。
dp[0][j]存放编号0的物品的时候各个容量的背包所能存放的最大价值。
那么很明显当 `j < weight[0]`的时候dp[0][j] 应该是 0因为背包容量比编号0的物品重量还小。
`j >= weight[0]`时,**dp[0][j] 如果能放下weight[0]的话,就一直装,每一种物品有无限个**。
代码初始化如下:
```CPP
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];
```
(注意上面初始化和 [01背包理论基础二维数组](https://programmercarl.com/背包理论基础01背包-1.html)的区别在于物品有无限个)
此时dp数组初始化情况如图所示
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20241114161608.png)
dp[0][j] 和 dp[i][0] 都已经初始化了,那么其他下标应该初始化多少呢?
其实从递归公式: dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); 可以看出dp[i][j] 是由上方和左方数值推导出来了,那么 其他下标初始为什么数值都可以,因为都会被覆盖。
但只不过一开始就统一把dp数组统一初始为0更方便一些。
最后初始化代码如下:
```CPP
// 初始化 dp
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
for (int j = weight[0]; j <= bagWeight; j++) {
dp[0][j] = dp[0][j - weight[0]] + value[0];
}
```
### 4. 确定遍历顺序
[01背包理论基础二维数组](https://programmercarl.com/背包理论基础01背包-1.html) 中我们讲过01背包二维DP数组先遍历物品还是先遍历背包都是可以的。
因为两种遍历顺序对于二维dp数组来说递推公式所需要的值二维dp数组里对应的位置都有。
详细可以看 [01背包理论基础二维数组](https://programmercarl.com/背包理论基础01背包-1.html) 中的 【遍历顺序】的讲解
所以既可以 先遍历物品再遍历背包:
```CPP
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]);
}
}
```
也可以 先遍历背包再遍历物品:
```CPP
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
for (int i = 1; i < n; i++) { // 遍历物品
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]);
}
}
```
### 5. 举例推导dp数组
以本篇举例数据为例填满了dp二维数组如图
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20241126113752.png)
因为 物品0 的性价比是最高的,而且 在完全背包中每一类物品都有无限个所以有无限个物品0既然物品0 性价比最高当然是优先放物品0。
### 本题代码:
```CPP
#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数组讲解](./背包问题完全背包一维.md)
## 其他语言版本
### Java
```Java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int bagWeight = scanner.nextInt();
int[] weight = new int[n];
int[] value = new int[n];
for (int i = 0; i < n; i++) {
weight[i] = scanner.nextInt();
value[i] = scanner.nextInt();
}
int[][] dp = new int[n][bagWeight + 1];
// 初始化
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] = Math.max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);
}
}
}
System.out.println(dp[n - 1][bagWeight]);
scanner.close();
}
}
```
### Go
### Python
```python
def knapsack(n, bag_weight, weight, value):
dp = [[0] * (bag_weight + 1) for _ in range(n)]
# 初始化
for j in range(weight[0], bag_weight + 1):
dp[0][j] = dp[0][j - weight[0]] + value[0]
# 动态规划
for i in range(1, n):
for j in range(bag_weight + 1):
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])
return dp[n - 1][bag_weight]
# 输入
n, bag_weight = map(int, input().split())
weight = []
value = []
for _ in range(n):
w, v = map(int, input().split())
weight.append(w)
value.append(v)
# 输出结果
print(knapsack(n, bag_weight, weight, value))
```
### JavaScript
<p align="center">
<a href="https://programmercarl.com/other/kstar.html" target="_blank">
<img src="../pics/网站星球宣传海报.jpg" width="1000"/>
</a>