背包九讲源自dd大牛的博客《背包九讲》,经yxc大佬讲解和汇总,所有题目在这里
有 N 件物品和一个容量是 m 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
数据范围
0
状态表示:
用f(i, j)表示从前i个物品种选出体积小于等于j的所有选法中的最大价值。
如果f(i-1,j), f(i-1, j-1),……,f(i-1, j-v),……,f(i-1, 0)已经确定
则状态f(i, j)可以由f(i-1, j)或f(i-1, j-v)转移过来。
状态转移:
f ( i , j ) = { f ( i − 1 , j ) , 不 选 第 i 件 物 品 f ( i − 1 , j − v ) + w , 选 择 第 i 件 物 品 f(i, j)=\left\{ \begin{aligned} f(i-1, j), 不选第i件物品\\ f(i-1, j-v)+w, 选择第i件物品 \end{aligned} \right. f(i,j)={ f(i−1,j),不选第i件物品f(i−1,j−v)+w,选择第i件物品
状态转移方程为:f(i, j ) = max{f(i-1, j), f(i-1, j-v)+w}
#include
using namespace std;
const int N = 1010;//数据范围的限定,最多的物品和最大的体积
int n, m;//物品数和背包体积
int f[N][N];
int V[N], W[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];//体积为j时,不选择第i个物品的情况
if(j >= V[i]) {
//状态转移
//转移方程上面推导过
f[i][j] = max(f[i][j], f[i-1][j-V[i]]+W[i]);
}
}
}
cout<<f[n][m]<<endl;
return 0;
}
但是我们发现所有的f(i)的状态都是只由f(i-1)的状态转换而来的,如果我们取消掉f数组的第一维,只保存第二维。
当前i层可以用未被覆盖的i-1层的未被覆盖的数据来进行状态转移
转移方程为:f(j) = max(f(j), f(j-vi)+wi)
但是要注意,我们的第i层和第j层共有一个数组f,而我们需要的是未被覆盖过的第i-1层的数据更新第i层,所有需要逆序枚举体积j
#include
#include
using namespace std;
const int N = 1010;
int n, m;//物品数量和背包体积
int f[N];//表示体积为i的背包最多可以放下的物品价值
int main() {
scanf("%d%d", &n, &m);
int V, W;
for(int i = 0; i < n; ++i) {
scanf("%d%d", &V, &W);
//0-1背包问题,每个物品最多只能用一次,更新状态就需要用到上一层结果
for(int j = m; j >= V; --j)
f[j] = max(f[j], f[j-V]+W);
}
cout<<f[m]<<endl;
return 0;
}
有 N 件物品和一个容量是 m 的背包。每件物品可以使用无限次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
数据范围
0
完全背包问题和0-1背包问题的唯一差别就是其每种物品可以用无限次
状态表示
我们同样设f(i, j)表示从前i种物品种选出体积不超过j的所有选法的最大价值
则对于物品i和当前的体积j, 我们可选择0,1,2,…,r个。
(因为不能超出当前背包体积j,所以r = ⌊ l o g 2 ( j ) ⌋ \lfloor log _2(j) \rfloor ⌊log2(j)⌋)
于是我们可以得到类似于0-1背包的从状态i-1转移到状态i的状态转移方程,只是多了从0—r的一个循环,即
for(int k = 0; j -k * v >=0 ; k++) f[i][j] = max(f(i, j), f(i-1, j-k*v)+k*w)
#include
#include
using namespace std;
const int N = 1010;
int f[N][N];
int main() {
int n, m;//n种物品,体积为m
scanf("%d%d", &n, &m);
int v, w;
for(int i = 1; i <= n; ++i) {
scanf("%d%d", &v, &w);
for(int j = 0; j <= m;++j) {
for(int k = 0; k*v <= j; ++k) {
f[i][j] = max(f[i][j], f[i-1][j-k*v]+k*w);
}
}
}
printf("%d", f[n][m]);
return 0;
}
在朴素做法中的状态转移方程用一个循环表示,其实我们可以将循环展开,就像这样:
f(i, j) = max{ f(i-1, j), f(i-1, j-v)+w, f(i-1, j-2*v)+2*w, ... , f(i-1, j-r*v)+r*w}
同时我们展开另一项f(i, j-v)
因为要保证第二维大于0,所以他最终展开后的最后一项也一定是f(i-1, j-r*v)状态
f(i, j-v) = max{ f(i-1, j-v), f(i-1, j-2*v)+w, ..., f(i-1, j-r*v)+(r-1)*w}
通过上面两个式子我们就可发现,一个更简便的状态转移方程:
f(i, j) = max{f(i-1, j), f(i, j-v)+w}
此时我们只需要中计算过的f(i, j-v)
来进行状态转移,而无需计算所有的f(i-1, ...)
的状态
这次我们直接考虑像0-1背包的滚动数组一样直接直接用一维数组来优化空间,于是f(j) = max(f(j), f(j-v)+w)
这里我们需要的f(j-v)是第i层的j-v,是被更新过的,所有我们要正序枚举体积。
迭代结束后f(m)就是答案,他代表的就是把所有的n种物品放入体积为m的背包中的最大价值数。
#include
using namespace std;
const int N = 1010;
int n, m;
int f[N];
int main() {
cin>>n>>m;
for(int i = 0; i < n; ++i) {
int v, w;
cin>>v>>w;
for(int j = v; j <= m; ++j) {
f[j] = max(f[j], f[j-v]+w);
}
}
cout<<f[m]<<endl;
return 0;
}
有 N 种物品和一个容量是 m 的背包。
第 i 种物品有si个,体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
0i, vi,wi≤100
首先看到这题类似于完全背包问题,只是每种物品的数量不是无限,而是有限个。
考虑用朴素完全背包的做法,暴力进行s次循环的状态转移。
for(int k = 0; k <= s && j -k * v >=0 ; k++) f[i][j] = max(f(i, j), f(i-1, j-k*v)+k*w)
时间复杂度: O(nms)小数据可以ac
#include
using namespace std;
const int N = 110;
int f[N][N];
int main () {
int n, m;
cin>>n>>m;
for(int i = 1; i <= n; ++i){
int v, w, s;
cin>>v>>w>>s;
for(int j = 0; j <= m; ++j) {
for(int k = 0; k <= s; ++k) {
if(j >= k*v)
f[i][j] = max(f[i-1][j-k*v]+k*w, f[i][j]);
}
}
}
cout<<f[n][m]<<endl;
return 0;
}
0i, vi,wi≤2000
如过还是使用之前的朴素做法,O(nms)时间复杂度,最大计算4*109次,1s内不可能完成,需要考虑速度更快的做法。
对于某个s我们需要选择的是s以内的任意一个数量的物品,那么是否可以用更少的数来表示1–s内的所有数呢?
于是,我们将s个数进行拆分,分成1, 2, 4, 8, …,r, s-r
(另r = 2 ⌊ l o g 2 s ⌋ ^{\lfloor log _2s \rfloor} ⌊log2s⌋)
这些数是可以每个数至多用一次来表示出1—s以内的所有数,于是问题最后转换成了一个新的0-1背包问题。
#include
#include
using namespace std;
//由于数据范围已经扩展至1000种,2000的容积,2000个物品,朴素做法n*v*s的时间复杂度,会超时
//于是需要优化
//将s个物品进行拆分,拆成1,2,4,8,...s-2^((int)logs)为止,这些数一定可以组成1--s中的所有的数
//将原有问题转换成了新的0-1背包问题
//转换所有物品的时间复杂度nlogs,新的背包中物品的数量nlogs,
//n*logs < 1000*log2000 < 25000
//求解0--1背包问题时间复杂度v*nlogs,可以接受
const int N = 25000;
int f[N], V[N], W[N];
int n, m;
int cnt = 1;
int main() {
cin>>n>>m;
//问题转换
for(int i = 1; i <= n; ++i) {
int v, w, s;
scanf("%d%d%d", &v, &w, &s);
for(int j = 1; j <= s; s-=j, j *= 2){
V[cnt] = v*j;
W[cnt++] = w*j;
}
if(s > 0) {
V[cnt] = v*s;
W[cnt++] = w*s;
}
}
//0-1背包问题求解
for(int i = 1; i < cnt; ++i) {
for(int j = m; j >= V[i]; --j) {
f[j] = max(f[j-V[i]]+W[i], f[j]);
}
}
cout<<f[m]<<endl;
return 0;
}
0i, vi,wi≤20000
注:本题最难,最后转换成滑动窗口问题,用单调队列优化,等先更新完滑动窗口问题再写
背包问题三其实是和男人八题中的 Coins 问题是一样的
有 N 种物品和一个容量是 m 的背包。
物品一共有三类:
第一类物品只能用1次(0-1背包);
第二类物品可以用无限次(完全背包);
第三类物品最多只能用 si 次(多重背包);
每种体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
状态表示
其实本题只是前面三个基本背包问题的糅合,状态表示仍然是:从前i个物品中选,总体积不超过j的所有选法
状态计算
状态计算仍然沿用上面三类背包问题的状态转移方程, 只需要在获取数据时判断一下是何种问题选择对应的方程求解即可。
(此处对于多重背包问题采用二进制优化转换成0-1背包问题,于是状态转移只有两种了)
此处复习一下两种基本的状态转移方程
0-1背包:
需要上一层的状态,所有这里的体积逆序枚举:
f(j) = max(f(j), f(j-v)+w)
完全背包:
需要本层更新完的状态,顺序枚举体积:
f(j) = max(f(j), f(j-v)+w)
(具体的推导在上面)
#include
#include
using namespace std;
const int N = 1010;
int f[N];
int main() {
int n, m;
scanf("%d%d",&n, &m);
for(int i = 1; i <= n; ++i) {
int v, w, s;
scanf("%d%d%d", &v, &w, &s);
if(s == 0) {
//完全背包问题
for(int j = v; j <= m; ++j) {
f[j] = max(f[j], f[j-v]+w);
}
}
else {
//0-1或多重背包
//先将0-1背包看成特殊的多重背包
if(s == -1) s = 1;
//将多重背包进行二进制优化转换为0-1背包问题
int V[N],W[N],cnt = 0;
for(int k = 1; k < s; s -= k, k *= 2){
V[cnt] = k*v;
W[cnt] = k*w;
cnt++;
}
if(s > 0) {
V[cnt] = s*v;
W[cnt] = s*w;
cnt++;
}
//利用0-1背包的状态方程求解
for(int k = 0; k < cnt; k++) {
for(int j = m; j >= V[k]; j--) {
f[j] = max(f[j], f[j-V[k]]+W[k]);
}
}
}
}
//输出结果
printf("%d\n", f[m]);
return 0;
}
有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。
每件物品只能用一次。体积是 vi,重量是 mi,价值是 wi。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。
输出最大价值。
二维费用的背包问题只是比普通的0-1背包问题多了一个限制条件:质量,于是在状态表示和转移时加上这一条件即可。
状态表示
f(i, j, k)表示所有只从前i个物品中选择,且总体积不超过j,总质量不超过k的所有选法
状态转移
f ( i , j , k ) = { f ( i − 1 , j , k ) , 不 选 择 第 i 件 物 品 f ( i − 1 , j − v i , k − m i ) + w i , 选 择 第 i 件 f(i, j, k)=\left\{ \begin{aligned} f(i-1, j, k), 不选择第i件物品\\ f(i-1, j-v_i, k-m_i)+w_i,选择第i件 \end{aligned} \right. f(i,j,k)={ f(i−1,j,k),不选择第i件物品f(i−1,j−vi,k−mi)+wi,选择第i件
即f(i, j, k) = max(f(i-1, j, k), f(i-1, j-v, k-m)+w)
#include
using namespace std;
const int N = 110;
int f[N][N];
int main() {
int n, V, M;
cin>>n>>V>>M;
for(int i = 0; i < n; ++i) {
int v, m, w;
cin>>v>>m>>w;
for(int j = V; j >= v; --j) {
for(int k = M; k >= m; --k) {
f[j][k] = max(f[j][k], f[j-v][k-m]+w);
}
}
}
cout<<f[V][M]<<endl;
return 0;
}