含义
这里有 N 个物品,每个物品的体积和价值分别为 vi、wi,我们有一个容量为 V 的背包,求这个背包能装的物品最大价值是多少。(装的所有物品总体积不能超过 V)
四种最常见的背包问题:
-
01背包 : 每件物品最多只用一次
-
完全背包 : 每件物品有无限个
-
多重背包 : 每件物品最多有 Si 个
-
分组背包 : 物品有 N 组,每一组里有物品若干个,每一组最多只能拿一个物品
01背包
每件物品最多只用一次(要么用一次、要么不用)
f[i,j] 代表只从前 i 件物品选,并且总体积 ≤ j的所有选法的集合,其中 f[i,j] 的值是这些所有选法中价值最大的价值。
下面来想想 f[i,j] 这个集合怎么划分
将 f[i,j] 集合分为两部分:即第 i 个物品选 0 个还是 1 个
① 不含 i ,说明我们需要从前 i - 1 个物品中选且总体积 ≤ j 的所有选法的集合,即不含 i 的最大值为 f[i-1,j] 。
② 含 i ,说明我们需要从前 i 个物品中选且含 i 且总体积 ≤ j 的所有选法的集合,直接求不好求,我们可以先将第 i 个物品去掉,那么集合最大价值就应该为 f[i-1,j-vi],再将第 i 个物品的价值加上,即含 i 的最大值为 f[i-1,j-vi] + wi 。
综上,f[i,j] = max(f[i-1,j] , f[i-1,j-vi] + wi)
例题: AcWing 2. 01背包问题
#include<iostream>
#include<cmath>
using namespace std;
const int N = 1010;
int n,m;
int v[N],w[N]; //体积、价值
int f[N][N];
int main()
{
cin >> n >> m;
for(int i=1; i<=n; i++) cin >> v[i] >> w[i];
for(int i=1; i<=n; i++)
for(int j=0; j<=m; j++) //枚举所有体积
{
f[i][j] = f[i-1][j];
if(j>=v[i]) f[i][j] = max(f[i][j], f[i-1][j-v[i]] + w[i]);
}
cout << f[n][m];
}
二维到一维空间优化
for(int i=1; i<=n; i++)
for(int j=m; j>=v[i]; j--) //枚举所有体积
f[j] = max(f[j], f[j-v[i]] + w[i]);
cout << f[m];
完全背包
每件物品有无限个
01 背包是按照第 i 个物品选 0 个还是 1 个划分 f[i,j] 的,那么完全背包就可以按照第 i 个物品选多少个来划分,0/1/2/3…/k(k 个是最多能装的)
含第 i 个物品 k 个(k=0/1/2/3…),直接求不好求,可以先将 k 个物品 i 去掉,那么集合最大价值就应该为 f[i - 1,j - k * v[i]] ,再将第 k 个第 i 个物品的价值加上,即 f[i - 1,j - k * v[i]] + k * w[i]。
综上,f[i,j] = f[i - 1,j - k * v[i]] + k * w[i]。
超时代码: 三层循环,第一层循环执行 n 次,第二层循环执行 m 次,第三层循环最多执行 m 次,所以,n*m2=109,是超时的。
#include<iostream>
#include<cmath>
using namespace std;
const int N = 1010;
int n,m;
int v[N],w[N]; //体积、价值
int f[N][N];
int main()
{
cin >> n >> m;
for(int i=1; i<=n; i++) cin >> v[i] >> w[i];
for(int i=1; i<=n; i++) //超时代码
for(int j=0; j<=m; j++) //枚举所有体积
for(int k=0; k*v[i]<=j; k++)
f[i][j] = max(f[i][j],f[i-1][j-k*v[i]] + k*w[i]);
cout << f[n][m];
}
那如何去优化呢?
所以,f[i,j] = max(f[i - 1,j],f[i,j-v[i]]+w),这样我们本来需要枚举 k 个状态,现在只需要枚举 2 个状态即可,执行106次即可。
#include<iostream>
#include<cmath>
using namespace std;
const int N = 1010;
int n,m;
int v[N],w[N]; //体积、价值
int f[N][N];
int main()
{
cin >> n >> m;
for(int i=1; i<=n; i++) cin >> v[i] >> w[i];
for(int i=1; i<=n; i++)
for(int j=0; j<=m; j++) //枚举所有体积
{
f[i][j] = f[i-1][j];
if(j>=v[i]) f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i]);
}
cout << f[n][m];
}
二维到一维空间优化
for(int i=1; i<=n; i++)
for(int j=v[i]; j<=m; j++) //枚举所有体积
f[j] = max(f[j],f[j-v[i]]+w[i]);
cout << f[m];
多重背包
每件物品最多有 Si 个
多重背包的集合划分方式和上一题完全背包(超时的那个方法)是一样的,只是 k 有限制条件了,k 需要≤ s[i]
#include<iostream>
#include<cmath>
using namespace std;
const int N = 110;
int n,m;
int v[N],w[N],s[N]; //体积、价值、数量
int f[N][N];
int main()
{
cin >> n >> m;
for(int i=1; i<=n; i++) cin >> v[i] >> w[i] >>s[i];
for(int i=1; i<=n; i++)
for(int j=0; j<=m; j++) //枚举所有体积
for(int k=0; k<=s[i] && k*v[i]<=j; k++)
{
f[i][j] = max(f[i][j],f[i-1][j-k*v[i]] + k*w[i]);
}
cout << f[n][m];
}
当数据范围变大了,用上面的三层循环做的话肯定是会超时的,我们有一种二进制的优化方法
我们可以把这个 多重背包 问题转换成 01背包 问题。
假如一个物品有 s 件,那么可以把这 s 件拆分 s 份,每份 1 个,然后对所有物品都这样拆分,拆分后的每个物品最多只能用一次,那么就转换成了01背包问题。
可是,这样拆,还是会超时的,因为,每个物品最多拆成 2000 份,所有物品拆完后一共有 1000*2000=2000000 个物品,V最大是2000,这样的话,两层循环达到109,还是超时的。
我们其实不需要拆成这么多份的,只需要将 s 转换为 log2s份即可(上取整)。
假设一个物品有 7 件,那么这七件是有从 0~7 八种选法的,我们其实可以拆分成 1、2、4 (20、21、22)这样三份,这三份的一些组合就可以将这八种选法全部表示出来:
0 = 一个都不选
1 = 1
2 = 2
3 = 1 + 2
4 = 4
5 = 1 + 4
6 = 2 + 4
7 = 1 + 2 + 4
时间复杂度:NVlog2S=1000 * 2000 * 11 = 2*107
#include<iostream>
#include<cmath>
using namespace std;
const int N = 1010;
int n,m;
int v[N],w[N]; //体积、价值
int f[N][N];
int main()
{
cin >> n >> m;
int cnt = 0;
for(int i=1; i<=n; i++)
{
int a,b,s;
cin >> a >> b >> s;
int k = 1;
while(k <= s)
{
cnt++;
v[cnt] = a * k;
w[cnt] = b * k;
s -= k;
k *= 2;
}
if(s > 0)
{
cnt++;
v[cnt] = a * s;
w[cnt] = b * s;
}
}
n = cnt;
for(int i=1; i<=n; i++)
for(int j=0; j<=m; j++)
{
f[i][j] = f[i-1][j];
if(j >= v[i]) f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);
}
cout<<f[n][m];
}
分组背包
物品有 N 组,每一组里有物品若干个,每一组最多只能拿一个物品
f[i,j] 代表从前 i 组选,且总体积不超过 j 的所有选法的集合。
集合划分:
① 不选这一组,即 f[i-1,j]
② 选这一组的第 k 个,即 f[i-1,j-v[i][k]+w[i][k]]
#include<iostream>
#include<algorithm>
#include<cmath>
using namespace std;
const int N = 110;
int n,m;
int v[N][N],w[N][N],s[N]; //体积、价值
int f[N][N];
int main()
{
cin >> n >> m;
for(int i=1; i<=n; i++)
{
cin >> s[i];
for(int j=0; j<s[i]; j++)
cin >> v[i][j] >> w[i][j];
}
for(int i=1; i<=n; i++) //枚举每一组
for(int j=0; j<=m; j++) //枚举所有体积
{
f[i][j] = f[i-1][j]; //不选这一组
for(int k=0; k<s[i]; k++) //选这组:枚举所有选择
if(v[i][k] <= j)
f[i][j] = max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]);
}
cout << f[n][m];
}
二维到一维空间优化
for(int i=1; i<=n; i++) //枚举每一组
for(int j=m; j>=0; j--) //枚举所有体积
for(int k=0; k<s[i]; k++) //选这组:枚举所有选择
if(v[i][k] <= j) f[j] = max(f[j],f[j-v[i][k]]+w[i][k]);
cout << f[m];