Files
python/TangDou/AcWing/BeiBao/【总结】01背包与完全背包专题.md
HuangHai 1f397eca87 'commit'
2025-08-30 18:35:01 +08:00

277 lines
8.3 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.

### 一、$01$背包
#### 1、状态定义
$f[i][j]$ 代表在前$i$个物品中选择,背包容量上限是$j$的情况下,可以获得的最大价值。
#### 2、状态转移
$$\large f[i][j] = max(f[i-1][j], f[i - 1][j - w] + v)$$
**解释**
我们带着一个剩余容量为$j$的背包来到第$i$个物品面前
- ① 如果我们不选择第$i$个物品,那么我们的收益就只能在前$i-1$个物品中获取到,可以使用的空间没有变化,还是$j$,即可以从$f[i-1][j]$转移过来
- ② 如果我们选择了第$i$个物品,那么就会要求在走完前$i-1$个物品时,剩余的空间是$j-v$,这样才能放得下第$i$个物品,即可以从$f[i-1][j-w]+v$获取到现在的最大价值
两个选择需要$PK$选一下大王,才是最终可能获取到的最大值,即
$$\large f[i][j]=max(f[i-1][j],f[i-1][j-w]+v)$$
#### 3、实现代码(二维)
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m;
int f[N][N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int w, v;
cin >> w >> v;
for (int j = 1; j <= m; j++) {
f[i][j] = f[i - 1][j];
if (j >= w)
f[i][j] = max(f[i][j], f[i - 1][j - w] + v); // 两种情况取最大值
}
}
printf("%d\n", f[n][m]);
return 0;
}
```
#### 4、实现代码(一维)
上面的二维实现中,我们通过瞪眼大法(观察法)得知,打表时每一行的数据,都只依赖于上一行,即$f[i][?]$一定是从$f[i-1][?]$转移过来,也就再向上的其它数据行变得没有用处,我们可以重复利用,这样就可以使用滚动数组或者降维处理。但由于后面的依赖于前面的,如果是正序枚举就会造成后面使用时,前面的数据已修改造成统计错误,办法就是从后向前枚举,以保证在后面的数据处理时,前面的数据还没有修改,是上一行的数据,就正确了。
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m;
int f[N];
int main() {
cin >> n >> m;
// 01背包模板
for (int i = 1; i <= n; i++) {
int w, v;
cin >> w >> v;
for (int j = m; j >= w; j--)
f[j] = max(f[j], f[j - w] + v);
}
printf("%d\n", f[m]);
return 0;
}
```
### 二、完全背包
#### 1、状态定义
同$01$背包
#### 2、状态转移
第$i$个物品,我们可以选择,也可以放弃,放弃了自然就是$f[i−1][j]$,这个没什么好讲。
如果我们选择了$i$物品,肯定要带上它的价值$v$,而承担它的重量$w$,
完成了选择它以后,我们接下来可以在前多少个物品中选呢?其实,还是$i$个,因为每种物品的数量无限啊!所以,选完了$i$,剩余的表示还是:$f[i][j−w]$。
$$\large f[i][j]=max(f[i-1][j],f[i][j-w]+v)$$
#### 3、实现代码(二维)
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m;
int f[N][N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int w, v;
cin >> w >> v;
for (int j = 1; j <= m; j++) {
f[i][j] = f[i - 1][j];
if (j >= w)
f[i][j] = max(f[i][j], f[i][j - w] + v);
}
}
printf("%d\n", f[n][m]);
return 0;
}
```
#### 4、实现代码(一维)
再用二维降一维的思路来思考,就得到了完全背包的终极解法:
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m;
int f[N];
// 完全背包问题
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int w, v;
cin >> w >> v;
for (int j = w; j <= m; j++)
f[j] = max(f[j], f[j - w] + v);
}
printf("%d\n", f[m]);
return 0;
}
```
### 三、练习题
![](http://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/2023/03/3313c9bfecc7482b8d3e60a52928039b.jpg)
![](http://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/2023/03/242f1dbabd8057cc4b1a3b2892538af0.jpg)
### 四、进阶【二维费用完全背包】
**[前导知识 $AcWing$ $8$. 二维费用的背包问题](https://www.acwing.com/problem/content/description/8/)**
![](http://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/2023/03/4382f55a587b9e5cb251e767fb6ef4cf.png)
```cpp {.line-numbers}
#include <bits/stdc++.h>
// https://blog.csdn.net/gnn8291/article/details/90632100
using namespace std;
int W; // 可以提起 W单位重量的东西,
int V; // 有一个能装V个单位体积的购物袋
int n; // 为商品种类数
// 注意多维限制的01背包,一般N值都比较小,100左右,太大就会编译出错了。
const int N = 110;
// 某种商品的重量、体积和让利金额
int a[N], b[N], c[N];
// DP数组
int f[N][N][N];
/**
10 9 4
8 3 6
5 4 5
3 7 7
4 5 4
答案:
9
2 4
*/
int main() {
// 输入
cin >> W >> V >> n;
// 每一种商品的重量、体积和让利金额
for (int i = 1; i <= n; i++) cin >> a[i] >> b[i] >> c[i];
// 遍历每个种类
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= W; j++) { // 遍历重量
for (int k = 1; k <= V; k++) { // 遍历体积
int x = f[i - 1][j - a[i]][k - b[i]] + c[i]; // 如果选了,结果是x
int y = f[i - 1][j][k]; // 如果不选,结果是y
// 如果剩余的重量和体积都够用的时候,尝试拿当前物品
if (j >= a[i] && k >= b[i])
f[i][j][k] = max(x, y); // 也是两个分支,一是不选,另一个是选,要取最大值
else
f[i][j][k] = y; // 装不下,不管是哪种原因装不下,都是这个分支
}
}
}
// 输出
cout << f[n][W][V] << endl; // 小惠能够得到的最大让利金额 
return 0;
}
```
带路径
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
int W; // 可以提起 w 单位重量的东西,
int V; // 有一个能装v个单位体积的购物袋
int n; // 为商品种类数
// 注意多维限制的01背包,一般N值都比较小,100左右,太大就会编译出错了。
const int N = 110;
// 某种商品的重量、体积和让利金额
int a[N], b[N], c[N];
// 在取得最大让利金额的时候,到底是拿了哪些商品?
string s[N][N][N];
// DP数组
int f[N][N][N];
/**
10 9 4
8 3 6
5 4 5
3 7 7
4 5 4
答案:
9
2 4
*/
int main() {
// 输入
cin >> W >> V >> n;
// 每一种商品的重量、体积和让利金额
for (int i = 1; i <= n; i++) cin >> a[i] >> b[i] >> c[i];
// 遍历每个种类
for (int i = 1; i <= n; i++) {
// 遍历重量(填表)
for (int j = 1; j <= W; j++) {
// 遍历体积(填表)
for (int k = 1; k <= V; k++) {
int x = f[i - 1][j - a[i]][k - b[i]] + c[i];
int y = f[i - 1][j][k];
// 如果剩余的重量和体积都够用的时候,尝试拿当前物品
if (j >= a[i] && k >= b[i]) {
// 也是两个分支,一是不选,另一个是选,要取最大值
if (x > y) { // 要上合适
f[i][j][k] = x;
// 同时记录拿了这个物品
s[i][j][k] = s[i - 1][j - a[i]][k - b[i]] + " " + (char)(i + '0');
} else // 不要合适
f[i][j][k] = y;
} else { // 装不下,不管是哪种原因装不下,都是这个分支
f[i][j][k] = y;
// 同时记录不拿当前物品,保持和之前一样的物品列表
s[i][j][k] = s[i - 1][j][k];
}
}
}
}
// 输出
cout << f[n][W][V] << endl; // 小惠能够得到的最大让利金额
string str = s[n][W][V]; // 依次为从小到大排列的商品序号
cout << str.substr(1, str.size() - 1);
return 0;
}
```