Files
leetcode-master/problems/背包理论基础01背包-1.md
2025-05-19 17:11:04 +08:00

594 lines
19 KiB
Markdown
Executable File
Raw Permalink 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.

* [做项目多个C++、Java、Go、测开、前端项目](https://www.programmercarl.com/other/kstar.html)
* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html)
* [背八股40天挑战高频面试题](https://www.programmercarl.com/xunlian/bagu.html)
# 动态规划01背包理论基础
本题力扣上没有原题,大家可以去[卡码网第46题](https://kamacoder.com/problempage.php?pid=1046)去练习,题意是一样的。
## 算法公开课
**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html)[带你学透0-1背包问题](https://www.bilibili.com/video/BV1cg411g7Y6/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。
## 思路
正式开始讲解背包问题!
对于面试的话其实掌握01背包和完全背包就够用了最多可以再来一个多重背包。
如果这几种背包,分不清,我这里画了一个图,如下:
![416.分割等和子集1](https://file1.kamacoder.com/i/algo/20210117171307407.png)
除此以外其他类型的背包面试几乎不会问都是竞赛级别的了leetcode上连多重背包的题目都没有所以题库也告诉我们01背包和完全背包就够用了。
而完全背包又是也是01背包稍作变化而来完全背包的物品数量是无限的。
**所以背包问题的理论基础重中之重是01背包一定要理解透**
leetcode上没有纯01背包的问题都是01背包应用方面的题目也就是需要转化为01背包问题。
**所以我先通过纯01背包问题把01背包原理讲清楚后续再讲解leetcode题目的时候重点就是讲解如何转化为01背包问题了**
之前可能有些录友已经可以熟练写出背包了,但只要把这个文章仔细看完,相信你会意外收获!
### 01 背包
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i]得到的价值是value[i] 。**每件物品只能用一次**,求解将哪些物品装入背包里物品价值总和最大。
这是标准的背包问题,以至于很多同学看了这个自然就会想到背包,甚至都不知道暴力的解法应该怎么解了。
这样其实是没有从底向上去思考,而是习惯性想到了背包,那么暴力的解法应该是怎么样的呢?
每一件物品其实只有两个状态取或者不取所以可以使用回溯法搜索出所有的情况那么时间复杂度就是O(2^n)这里的n表示物品数量。
**所以暴力的解法是指数级别的时间复杂度。进而才需要动态规划的解法来进行优化!**
在下面的讲解中,我举一个例子:
背包最大重量为4。
物品为:
| | 重量 | 价值 |
| ----- | ---- | ---- |
| 物品0 | 1 | 15 |
| 物品1 | 3 | 20 |
| 物品2 | 4 | 30 |
问背包能背的物品最大价值是多少?
以下讲解和图示中出现的数字都是以这个例子为例。
(为了方便表述,下面描述 统一用 容量为XX的背包放下容量重量为XX的物品物品的价值是XX
### 二维dp数组01背包
依然动规五部曲分析一波。
#### 1. 确定dp数组以及下标的含义
我们需要使用二维数组,为什么呢?
因为有两个维度需要分别表示:物品 和 背包容量
如图,二维数组为 dp[i][j]。
![动态规划-背包问题1](https://file1.kamacoder.com/i/algo/20210110103003361.png)
那么这里 i 、j、dp[i][j] 分别表示什么呢?
i 来表示物品、j表示背包容量。
如果想用j 表示物品i 表示背包容量 行不行? 都可以的,个人习惯而已)
我们来尝试把上面的 二维表格填写一下。
动态规划的思路是根据子问题的求解推导出整体的最优解。
我们先看把物品0 放入背包的情况:
![](https://file1.kamacoder.com/i/algo/20240730113455.png)
背包容量为0放不下物品0此时背包里的价值为0。
背包容量为1可以放下物品0此时背包里的价值为15.
背包容量为2依然可以放下物品0 (注意 01背包里物品只有一个此时背包里的价值为15。
以此类推。
再看把物品1 放入背包:
![](https://file1.kamacoder.com/i/algo/20240730114228.png)
背包容量为 0放不下物品0 或者物品1此时背包里的价值为0。
背包容量为 1只能放下物品0背包里的价值为15。
背包容量为 2只能放下物品0背包里的价值为15。
背包容量为 3上一行同一状态背包只能放物品0这次也可以选择物品1了背包可以放物品1 或者 物品0物品1价值更大背包里的价值为20。
背包容量为 4上一行同一状态背包只能放物品0这次也可以选择物品1了背包可以放下物品0 和 物品1背包价值为35。
以上举例是比较容易看懂我主要是通过这个例子来帮助大家明确dp数组的含义。
上图中,我们看 dp[1][4] 表示什么意思呢。
任取 物品0物品1 放进容量为4的背包里最大价值是 dp[1][4]。
通过这个举例我们来进一步明确dp数组的含义。
即**dp[i][j] 表示从下标为[0-i]的物品里任意取放进容量为j的背包价值总和最大是多少**。
**要时刻记着这个dp数组的含义下面的一些步骤都围绕这dp数组的含义进行的**如果哪里看懵了就来回顾一下i代表什么j又代表什么。
#### 2. 确定递推公式
这里在把基本信息给出来:
| | 重量 | 价值 |
| ----- | ---- | ---- |
| 物品0 | 1 | 15 |
| 物品1 | 3 | 20 |
| 物品2 | 4 | 30 |
对于递推公式,首先我们要明确有哪些方向可以推导出 dp[i][j]。
这里我们dp[1][4]的状态来举例:
求取 dp[1][4] 有两种情况:
1. 放物品1
2. 还是不放物品1
如果不放物品1 那么背包的价值应该是 dp[0][4] 即 容量为4的背包只放物品0的情况。
推导方向如图:
![](https://file1.kamacoder.com/i/algo/20240730174246.png)
如果放物品1 **那么背包要先留出物品1的容量**目前容量是4物品1 的容量就是物品1的重量为3此时背包剩下容量为1。
容量为1只考虑放物品0 的最大价值是 dp[0][1],这个值我们之前就计算过。
所以 放物品1 的情况 = dp[0][1] + 物品1 的价值,推导方向如图:
![](https://file1.kamacoder.com/i/algo/20240730174436.png)
两种情况分别是放物品1 和 不放物品1我们要取最大值毕竟求的是最大价值
`dp[1][4] = max(dp[0][4], dp[0][1] + 物品1 的价值) `
以上过程,抽象化如下:
* **不放物品i**背包容量为j里面不放物品i的最大价值是dp[i - 1][j]。
* **放物品i**背包空出物品i的容量后背包容量为j - weight[i]dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]且不放物品i的最大价值那么dp[i - 1][j - weight[i]] + value[i] 物品i的价值就是背包放物品i得到的最大价值
递归公式: `dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);`
#### 3. dp数组如何初始化
**关于初始化一定要和dp数组的定义吻合否则到递推公式的时候就会越来越乱**
首先从dp[i][j]的定义出发如果背包容量j为0的话即dp[i][0]无论是选取哪些物品背包价值总和一定为0。如图
![动态规划-背包问题2](https://file1.kamacoder.com/i/algo/2021011010304192.png)
在看其他情况。
状态转移方程 `dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);` 可以看出i 是由 i-1 推导出来那么i为0的时候就一定要初始化。
dp[0][j]i为0存放编号0的物品的时候各个容量的背包所能存放的最大价值。
那么很明显当 `j < weight[0]`的时候dp[0][j] 应该是 0因为背包容量比编号0的物品重量还小。
`j >= weight[0]`dp[0][j] 应该是value[0]因为背包容量放足够放编号0物品。
代码初始化如下:
```CPP
for (int i = 1; i < weight.size(); i++) { // 当然这一步如果把dp数组预先初始化为0了这一步就可以省略但很多同学应该没有想清楚这一点。
dp[i][0] = 0;
}
// 正序遍历
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}
```
此时dp数组初始化情况如图所示
![动态规划-背包问题7](https://file1.kamacoder.com/i/algo/20210110103109140.png)
dp[0][j] 和 dp[i][0] 都已经初始化了,那么其他下标应该初始化多少呢?
其实从递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出dp[i][j] 是由左上方数值推导出来了,那么 其他下标初始为什么数值都可以,因为都会被覆盖。
**初始-1初始-2初始100都可以**
但只不过一开始就统一把dp数组统一初始为0更方便一些。
如图:
![动态规划-背包问题10](https://file1.kamacoder.com/i/algo/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92-%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%9810.jpg)
最后初始化代码如下:
```CPP
// 初始化 dp
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}
```
**费了这么大的功夫才把如何初始化讲清楚相信不少同学平时初始化dp数组是凭感觉来的但有时候感觉是不靠谱的**
#### 4. 确定遍历顺序
在如下图中,可以看出,有两个遍历的维度:物品与背包重量
![动态规划-背包问题3](https://file1.kamacoder.com/i/algo/2021011010314055.png)
那么问题来了,**先遍历 物品还是先遍历背包重量呢?**
**其实都可以!! 但是先遍历物品更好理解**
那么我先给出先遍历物品,然后遍历背包重量的代码。
```CPP
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); 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 - 1][j - weight[i]] + value[i]);
}
}
```
**先遍历背包再遍历物品也是可以的注意我这里使用的二维dp数组**
例如这样:
```CPP
// weight数组的大小 就是物品个数
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
for(int i = 1; i < weight.size(); i++) { // 遍历物品
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
```
为什么也是可以的呢?
**要理解递归的本质和递推的方向**
`dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);` 递归公式中可以看出dp[i][j]是靠dp[i-1][j]和dp[i - 1][j - weight[i]]推导出来的。
dp[i-1][j]和dp[i - 1][j - weight[i]] 都在dp[i][j]的左上角方向(包括正上方向),那么先遍历物品,再遍历背包的过程如图所示:
![动态规划-背包问题5](https://file1.kamacoder.com/i/algo/202101101032124.png)
再来看看先遍历背包,再遍历物品呢,如图:
![动态规划-背包问题6](https://file1.kamacoder.com/i/algo/20210110103244701.png)
**大家可以看出虽然两个for循环遍历的次序不同但是dp[i][j]所需要的数据就是左上角根本不影响dp[i][j]公式的推导!**
但先遍历物品再遍历背包这个顺序更好理解。
**其实背包问题里两个for循环的先后循序是非常有讲究的理解遍历顺序其实比理解推导公式难多了**
#### 5. 举例推导dp数组
来看一下对应的dp数组的数值如图
![动态规划-背包问题4](https://file1.kamacoder.com/i/algo/20210118163425129.jpg)
最终结果就是dp[2][4]。
建议大家此时自己在纸上推导一遍看看dp数组里每一个数值是不是这样的。
**做动态规划的题目最好的过程就是自己在纸上举一个例子把对应的dp数组的数值推导一下然后在动手写代码**
很多同学做dp题目遇到各种问题然后凭感觉东改改西改改怎么改都不对或者稀里糊涂就改过了。
主要就是自己没有动手推导一下dp数组的演变过程如果推导明白了代码写出来就算有问题只要把dp数组打印出来对比一下和自己推导的有什么差异很快就可以发现问题了。
本题力扣上没有原题,大家可以去[卡码网第46题](https://kamacoder.com/problempage.php?pid=1046)去练习,题意是一样的,代码如下:
```CPP
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, bagweight;// bagweight代表行李箱空间
cin >> n >> bagweight;
vector<int> weight(n, 0); // 存储每件物品所占空间
vector<int> value(n, 0); // 存储每件物品价值
for(int i = 0; i < n; ++i) {
cin >> weight[i];
}
for(int j = 0; j < n; ++j) {
cin >> value[j];
}
// dp数组, dp[i][j]代表行李箱空间为j的情况下,从下标为[0, i]的物品里面任意取,能达到的最大价值
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
// 初始化, 因为需要用到dp[i - 1]的值
// j < weight[0]已在上方被初始化为0
// j >= weight[0]的值就初始化为value[0]
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}
for(int i = 1; i < weight.size(); i++) { // 遍历科研物品
for(int j = 0; j <= bagweight; j++) { // 遍历行李箱容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j]; // 如果装不下这个物品,那么就继承dp[i - 1][j]的值
else {
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
}
cout << dp[n - 1][bagweight] << endl;
return 0;
}
```
## 总结
背包问题 是动态规划里的经典类型题目,大家要细细品味。
可能有的同学并没有注意到初始化 和 遍历顺序的重要性,我们后面做力扣上背包面试题目的时候,大家就会感受出来了。
下一篇 还是理论基础我们再来讲一维dp数组实现的01背包滚动数组分析一下和二维有什么区别在初始化和遍历顺序上又有什么差异。
## 其他语言版本
### 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();
}
for (int j = 0; j < n; ++j) {
value[j] = scanner.nextInt();
}
int[][] dp = new int[n][bagweight + 1];
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = 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 - 1][j - weight[i]] + value[i]);
}
}
}
System.out.println(dp[n - 1][bagweight]);
}
}
```
### Python
```python
n, bagweight = map(int, input().split())
weight = list(map(int, input().split()))
value = list(map(int, input().split()))
dp = [[0] * (bagweight + 1) for _ in range(n)]
for j in range(weight[0], bagweight + 1):
dp[0][j] = value[0]
for i in range(1, n):
for j in range(bagweight + 1):
if j < weight[i]:
dp[i][j] = dp[i - 1][j]
else:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
print(dp[n - 1][bagweight])
```
### Go
```go
package main
import (
"fmt"
)
func main() {
var n, bagweight int // bagweight代表行李箱空间
fmt.Scan(&n, &bagweight)
weight := make([]int, n) // 存储每件物品所占空间
value := make([]int, n) // 存储每件物品价值
for i := 0; i < n; i++ {
fmt.Scan(&weight[i])
}
for j := 0; j < n; j++ {
fmt.Scan(&value[j])
}
// dp数组, dp[i][j]代表行李箱空间为j的情况下,从下标为[0, i]的物品里面任意取,能达到的最大价值
dp := make([][]int, n)
for i := range dp {
dp[i] = make([]int, bagweight + 1)
}
// 初始化, 因为需要用到dp[i - 1]的值
// j < weight[0]已在上方被初始化为0
// j >= weight[0]的值就初始化为value[0]
for j := weight[0]; j <= bagweight; j++ {
dp[0][j] = value[0]
}
for i := 1; i < n; i++ { // 遍历科研物品
for j := 0; j <= bagweight; j++ { // 遍历行李箱容量
if j < weight[i] {
dp[i][j] = dp[i-1][j] // 如果装不下这个物品,那么就继承dp[i - 1][j]的值
} else {
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i])
}
}
}
fmt.Println(dp[n-1][bagweight])
}
func max(x, y int) int {
if x > y {
return x
}
return y
}
```
### JavaScript
```js
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
let input = [];
readline.on('line', (line) => {
input.push(line);
});
readline.on('close', () => {
let [n, bagweight] = input[0].split(' ').map(Number);
let weight = input[1].split(' ').map(Number);
let value = input[2].split(' ').map(Number);
let dp = Array.from({ length: n }, () => Array(bagweight + 1).fill(0));
for (let j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}
for (let i = 1; i < n; i++) {
for (let 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 - 1][j - weight[i]] + value[i]);
}
}
}
console.log(dp[n - 1][bagweight]);
});
```
### C
```c
#include <stdio.h>
#include <stdlib.h>
int max(int a, int b) {
return a > b ? a : b;
}
int main() {
int n, bagweight;
scanf("%d %d", &n, &bagweight);
int *weight = (int *)malloc(n * sizeof(int));
int *value = (int *)malloc(n * sizeof(int));
for (int i = 0; i < n; ++i) {
scanf("%d", &weight[i]);
}
for (int j = 0; j < n; ++j) {
scanf("%d", &value[j]);
}
int **dp = (int **)malloc(n * sizeof(int *));
for (int i = 0; i < n; ++i) {
dp[i] = (int *)malloc((bagweight + 1) * sizeof(int));
for (int j = 0; j <= bagweight; ++j) {
dp[i][j] = 0;
}
}
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = 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 - 1][j - weight[i]] + value[i]);
}
}
}
printf("%d\n", dp[n - 1][bagweight]);
for (int i = 0; i < n; ++i) {
free(dp[i]);
}
free(dp);
free(weight);
free(value);
return 0;
}
```