背包问题
【背包九讲专题】https://www.bilibili.com/video/BV1qt411Z7nE?vd_source=57dbd16b8c7c2ad258cccce5966c5be8
1. 01 背包问题
题目描述
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大
逐个选择物品
- 不选,最优直接看前面空间i最大装多少
- 选,看选后剩余空间,用剩余空间看前一个最多装多少
判断1,2两个哪个多,每次选择都是当前容量下(选与不选i及其之前枚举元素)的局部最优
/*
f[i][j] 表示只看前i个物品,总体积j的情况下,总价值最大多少
result = max(f[n][0~V])
// 状态转移函数
f[i][j]:
1. 不选第 i 个物品,f[i][j] = f[i-1][j];
2. 选第 i 个物品,f[i][j] = f[i-1][j-v[i]] + w[i];
*/
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, V; // n: 物品数量, V: 背包容量
int v[N], w[N]; // v[i]: 第i个物品的体积, w[i]: 第i个物品的价值
int f[N][N]; // 动态规划数组
int main(){
// 输入物品数量和背包容量
cin >> n >> V;
// 输入每个物品的体积和价值
for(int i = 1; i <= n; i++) {
cin >> v[i] >> w[i];
}
// 初始化动态规划数组
// 可以不初始化,因为默认是0,但为了清晰可以显式初始化
memset(f, 0, sizeof(f));
// 动态规划填表
for(int i = 1; i <= n; i++){ // 遍历每个物品
for(int j = 0; j <= V; j++){ // 遍历每个可能的体积
f[i][j] = f[i-1][j]; // 不选第 i 个物品
if(j >= v[i]){
f[i][j] = max(f[i][j], f[i-1][j - v[i]] + w[i]); // 选第i个物品
}
}
}
// 寻找最大价值
int result = 0;
for(int j = 0; j <= V; j++){
result = max(result, f[n][j]);
}
// 输出结果
cout << result;
return 0;
}
核心状态转移函数
// 枚举每个物品
for(int i=0;i< n;i++){
// 枚举体积
for(int j<m;j>=v[i];j--){
f[j]=max[f[j],f[j-v[i]]+w[i]]
}
}
复杂度 mn
dp【i】【j】 表示从前面【i-1】个物品里任意取,放到容量为j的背包里的最大值,这里的任意取其实是可以取1个 取2个 或者i-1个全取。
总之就是 每一个dp【i】【j】都是背包容量为j状态下的最优解。
递推公式怎么理解?
其实动态规划思想都是逐步推结果,每一步都是最优解。
当前物品可以选则拿或者不拿:
不拿:背包容量和背包里物品总价值都没有变化,和上一个状态下的总价值相同。
拿:背包容量减少,背包里的物品总价值=上一个状态下的总价值+当前物品的价值
考虑 i 个 物品的 v 容量下最优解 = max(考虑i-1个物品的最优解,第i个物品的价值 +考虑 i-1 个 物品的 剩余空间 容量下最优解)
优化到一维数组
为了防止数组更新覆盖,且剩余容量一定小于当前容量。可以从后向前更新i值
/*
f[i][j] 表示只看前i个物品,总体积j的情况下,总价值最大多少
result = max(f[n][0~V])
// 逆推
f[i][j]:
1. 不选第 i 个物品,f[i][j] = f[i-1][j];
2. 选第 i 个物品,f[i][j] = f[i-1][j-v[i]] + w[i];
*/
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, V; // n: 物品数量, V: 背包容量
int v[N], w[N]; // v[i]: 第i个物品的体积, w[i]: 第i个物品的价值
int f[N]; // 动态规划数组
int main(){
// 输入物品数量和背包容量
cin >> n >> V;
// 输入每个物品的体积和价值
for(int i = 1; i <= n; i++) {
cin >> v[i] >> w[i];
}
// 初始化动态规划数组
// 可以不初始化,因为默认是0,但为了清晰可以显式初始化
memset(f, 0, sizeof(f));
// 动态规划填表
for(int i = 1; i <= n; i++){ // 遍历每个物品
for(int j = V; j >= v[i]; j--){ // 遍历每个可能的体积
// 只更新不同的部分
if(j >= v[i]){
f[j] = max(f[j], f[j - v[i]] + w[i]); // 选第i个物品
}
}
}
// 输出结果
cout << f[m];
return 0;
}
2. 完全背包问题
题目描述
给定 n
种物品,每种物品有一个体积 v[i]
和一个价值 w[i]
。你有一个容量为 V
的背包。每种物品可以选择无限次。求在不超过背包容量的情况下,能装入物品的最大总价值。
样例输入:
3 5
1 2
2 4
3 4
样例输出:
8
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, V; // n: 物品数量, V: 背包容量
int v[N], w[N]; // v[i]: 第i个物品的体积, w[i]: 第i个物品的价值
int f[N]; // 动态规划数组,f[j]表示容量为j时的最大价值
int main(){
// 输入物品数量和背包容量
cin >> n >> V;
// 输入每个物品的体积和价值
for(int i = 1; i <= n; i++) {
cin >> v[i] >> w[i];
}
// 初始化动态规划数组
memset(f, 0, sizeof(f));
// 动态规划填表
for(int i = 1; i <= n; i++){ // 遍历每个物品
for(int j = v[i]; j <= V; j++){ // 遍历每个可能的体积,从小到大
f[j] = max(f[j], f[j - v[i]] + w[i]); // 选择或不选择第i个物品
}
}
// 输出结果
cout << f[V];
return 0;
}
为什么从小到大遍历
剩余容量小于j,含i的剩余容量的最优解先被考虑到。
考虑 i 个 物品的 v 容量下最优解 = max(考虑i-1个物品的最优解,第i个物品的价值 +考虑 i 个 物品的 剩余空间 下最优解)
数学归纳法:
- 考虑前i-1个物品之后,所有$f[j]$都是正确的
- 来证明,考虑完第i个物品后,所有$f[j]$也都是正确的
对于某个j而言,如果最优解中包含k个v[i]
$f[j]=max(f[j],f[j−k⋅v[i]]+k⋅w[i])$
则k-1的剩余容量的最优解会被先考虑到
$f[j]=max(f[j],f[j−(k-1)⋅v[i]]+(k-1)⋅w[i])$
3. 多重背包问题
对物品数量有限制
题目描述:
你有一个容量为 V
的背包,以及 N
种物品。每种物品有如下属性:
- 体积:
v[i]
- 价值:
w[i]
- 数量:
c[i]
每种物品可以选择的数量不超过 c[i]
个。你的目标是在不超过背包容量的情况下,选择物品使得总价值最大化。
3 7 // V N
1 1 3 // v w c
2 3 2
3 4 1
样例输出:
9
基本的方程只需将完全背包问题的方程略微一改即可,因为对于第i种物品有n[i]+1
种策略:取0件,取1件……取 n[i]
件。注意需要j
逆序,因为按照01背包考虑 取0件,取1件 是不同的状态,不能嵌套进自己的状态
可以两种初始转移状态
// 初始0状态所有容量的背包为0
1. f[i] = 0;
最大值 max = f[m]
// 初始0状态 容量为0的背包 为 0,其他容量的背包为-INF 不可达
2. f[0] = 0; f[i] = -INF, i!=0;
max(f[0...m])
第二种仅在剩余空间检查有恰好可以容纳的物品时转移,不允许有未使用的剩余空间,在向后遍历剩余空间有冗余时,不再转移更新,所以最大背包可能为-INF,需要向前查找
- 初始化方式一:所有
f[i]
初始化为0
,最终结果为f[m]
// 初始化
for(int i = 0; i <= m; i++) {
f[i] = 0;
}
// 动态规划填表
for(int i = 0; i < n; i++) { // 枚举每个物品
for(int j = m; j >= v[i]; j--) { // 枚举背包容量
for(int k = 1; k <= c[i] && k * v[i] <= j; k++) { // 枚举物品数量
f[j] = max(f[j], f[j - k * v[i]] + k * w[i]);
}
}
}
// 最终答案
printf("%d", f[m]);
- 初始化方式二:
f[0] = 0
,其余f[i] = -INF
,最终结果为max(f[0...m])
// 初始化
f[0] = 0;
for(int i = 1; i <= m; i++) {
f[i] = -INF; // 表示不可达状态
}
// 动态规划填表
for(int i = 0; i < n; i++) { // 枚举每个物品
for(int j = m; j >= v[i]; j--) { // 枚举背包容量
for(int k = 1; k <= c[i] && k * v[i] <= j; k++) { // 枚举物品数量
if(f[j - k * v[i]] != -INF) { // 确保状态可达
f[j] = max(f[j], f[j - k * v[i]] + k * w[i]);
}
}
}
}
// 最终答案
int max_val = -INF;
for(int i = 0; i <= m; i++) {
max_val = max(max_val, f[i]);
}
printf("%d", max_val);
二进制优化
将 k 分为 01背包问题的几项,1、2、4
对于任意最终到达的k,可以转为是否选择 路径上 1 2 4...、【k-log(k)】 来表示
分割时最后一组不足以翻倍时可以直接加入
#include <stdio.h>
#include <stdlib.h>
#define MAX_V 20000 // 根据具体问题调整背包容量上限
#define MAX_N 100 // 根据具体问题调整物品数量上限
// 定义物品结构体
typedef struct {
int v; // 体积
int w; // 价值
int c; // 数量
} Item;
// 函数声明
int max(int a, int b);
int main() {
int V, N;
Item items[MAX_N];
int f[MAX_V + 1]; // 动态规划数组
// 输入背包容量和物品数量
scanf("%d %d", &V, &N);
// 输入每个物品的体积、价值和数量
for(int i = 0; i < N; i++) {
scanf("%d %d %d", &items[i].v, &items[i].w, &items[i].c);
}
// 初始化DP数组
for(int i = 0; i <= V; i++) {
f[i] = 0;
}
// 动态规划填表,采用二进制优化
for(int i = 0; i < N; i++) { // 枚举每个物品
int v = items[i].v;
int w = items[i].w;
int c = items[i].c;
// 二进制拆分
for(int k = 1; c > 0; k <<= 1) { // k 每次翻倍,相当于 1, 2, 4, 8, ...
int mul = k < c ? k : c; // 当前拆分的数量
// 枚举背包容量,倒序遍历以防止重复计算
for(int j = V; j >= mul * v; j--) {
f[j] = max(f[j], f[j - mul * v] + mul * w);
}
c -= mul; // 更新剩余的数量
}
}
// 输出最终答案
printf("%d\n", f[V]);
return 0;
}
// 辅助函数:取两个数的最大值
int max(int a, int b) {
return (a > b) ? a : b;
}