01背包
问题模型
n种物品,每种物品有1个,
放入容量为m的背包中,每个物品有体积
v
[
i
]
v[i]
v[i]和价值
w
[
i
]
w[i]
w[i]
求放入背包的最大价值
基本方法
- n表示物品总数,m表示背包容量,v[]表示物品体积,w[]表示物品价值
- f[i][j]表示将前i个物品放入容量为j的背包能获得的最大价值
- f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j − v [ i ] ] + w [ i ] , f [ i − 1 ] [ j ] ) ; f[i][j]=max(f[i-1][j-v[i]]+w[i],f[i-1][j]); f[i][j]=max(f[i−1][j−v[i]]+w[i],f[i−1][j]);
- 边界: f [ 0 ] [ j ] = 0 f[0][j]=0 f[0][j]=0
- 解:
f
[
n
]
[
m
]
f[n][m]
f[n][m]
降维
0 | 1 | 2 | … | <-i | |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | |
1 | x | x | x | x | |
2 | x | x | x | x | |
3 | x | x | x | x | |
… | … | … | … | … | |
j |
参考代码:
for(int i=1;i<=n;i++){
for(int j=m;j>=v[i];j--){
if(f[j-v[i]]+w[i]>f[j])
f[j]=f[j-v[i]]+w[i];
}
}
完全背包
问题模型
n种物品,每种物品有无数个,
放入容量为m的背包中,每个物品有体积
v
[
i
]
v[i]
v[i]和价值
w
[
i
]
w[i]
w[i]
求放入背包的最大价值
方法1
伪代码:
//边界是f[0][j]=0
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
for(int k=0;k<=j/v[i];j++){
f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+w[i]*k);
}
}
}
//解是f[n][m]
时间复杂度很高!
方法2
边界和解都一样
f
[
i
]
[
j
]
f[i][j]
f[i][j]的定义还一样
此时考虑不选或者至少选一个:
- 不选的状态就是 f [ i − 1 ] [ j ] f[i-1][j] f[i−1][j]
- 选的话,先选一个再说: f [ i ] [ j − v [ i ] ] + w [ i ] f[i][j-v[i]]+w[i] f[i][j−v[i]]+w[i]
- 第二种情况,如果真的某个物品只选1个会不会出问题呢?
- 不会的,因为如果真的第i个物品只选一个,则 f [ i ] [ j − v [ i ] ] = f [ i − 1 ] [ j − v [ i ] ] f[i][j-v[i]]=f[i-1][j-v[i]] f[i][j−v[i]]=f[i−1][j−v[i]], f [ i ] [ j ] f[i][j] f[i][j]包含了 f [ i − 1 ] [ j ] f[i-1][j] f[i−1][j],不会出问题
综上所述,
f
[
i
]
[
j
]
=
max
(
f
[
i
−
1
]
[
j
]
,
f
[
i
]
[
j
−
v
[
i
]
]
+
w
[
i
]
)
;
f[i][j]=\max(f[i-1][j],f[i][j-v[i]]+w[i]);
f[i][j]=max(f[i−1][j],f[i][j−v[i]]+w[i]);
伪代码:
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
if(j>=v[i])
f[i][j]=max(f[i-1][j]/*不选*/,f[i][j-v[i]]+w[i]/*选至少一个*/);
else
f[i][j]=f[i-1][j];
}
}
}
降了一层循环,真NICE!
0 | 1 | 2 | … | <-i | |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | |
1 | x | x | x | x | |
2 | x | x | x | x | |
3 | x | x | x | x | |
… | … | … | … | … | |
j |
降维
研究表格,发现可以降维:
f
[
j
]
=
m
a
x
(
f
[
j
]
,
f
[
j
−
v
[
i
]
]
+
w
[
i
]
)
;
f[j]=max(f[j],f[j-v[i]]+w[i]);
f[j]=max(f[j],f[j−v[i]]+w[i]);
参考代码:
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]);
}
}
多重背包
问题模型
n种物品,每种物品有c[i]个,
放入容量为m的背包中,每个物品有体积
v
[
i
]
v[i]
v[i]和价值
w
[
i
]
w[i]
w[i]
求放入背包的最大价值
基本方法
f
[
i
]
[
j
]
f[i][j]
f[i][j]的定义不变
c
[
i
]
c[i]
c[i]表示第i个物品有多少个
伪代码:
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
for(int k=0;k<=min(c[i],j/v[i]);k++){
f[i][j]=max(f[i-1][j-k*v[i],k*w[i]);
}
}
}
降维
0 | 1 | 2 | … | < − i <-i <−i | |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | |
1 | x | x | x | x | |
2 | x | x | x | x | |
3 | x | x | x | x | |
… | … | … | … | … | |
j |
参考代码:
for(int i=1;i<=n;i++){
for(int j=m;j>=0;j--){
for(int k=0;k<=min(c[i],j/v[i]);k++){
if(f[j-k*v[i]]+k*w[i]>f[j])
f[j]=f[j-k*v[i]]+k*w[i];
}
}
}
解法二——转01背包
第i种物品有c[i]个,可以转化成c[i]中物品,每种物品有
1
1
1个
这样将多重背包转成了01背包
参考代码:
for(int i=1;i<=n;i++){
for(int k=1;k<=c[i];k++){//k从1开始,易错
for(int j=m;j>=v[i];j--){
if(f[j-v[i]]+w[i]>f[j])
f[j]=f[j-v[i]]+w[i];//注意这里是01背包的状态转移方程
}
}
}
- 注意:
只能在选同种物品时价值不受物品选的个数的限制时使用
混合背包
有些物品只能选1个,有些物品只能选
c
[
i
]
c[i]
c[i]个,有些物品能选无数个
因为所有的背包的边界、解和定义都一样,所以可以根据具体的数量规定具体的状态转移方程
参考代码:
//c[i]=0表示有无数个
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
if(c[i]==1){//01背包
if(j>=c[i])
f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
else
f[i][j]=f[i-1][j];
}
else if(c[i]==0){//完全背包
if(j>=c[i])
f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i]);
else
f[i][j]=f[i-1][j];
}
else{//混合背包
for(int k=0;k<=min(c[i],j/v[i]);k++)
f[i][j]=max(f[i-1][j],f[i-1][j-k*v[i]]+k*w[i]);
}
}
}
降维也要根据不同的背包问题,选择不同的方式
//c[i]=0表示有无数个
for(int i=1;i<=n;i++){
if(c[i]==1){//01背包
for(int j=m;j>=v[i];j--)
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
else if(p[i]==0){//完全背包
for(int j=v[i];j<=m;j++)
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
else{
for(int j=m;j>=0;j--){//多重背包
for(int k=0;k<=min(c[i],j/v[i]);k++)
f[j]=max(f[j],f[j-k*c[i]]+k*w[i]);
}
}
}
第二种方式
其实,因为背包容量最大也只能是m,所以理论上能选无数个的物品最多也只能选
m
/
v
[
i
]
m/v[i]
m/v[i]个
01背包可以看成
c
[
i
]
=
1
c[i]=1
c[i]=1的多重背包
这样,我们可以将三种背包都变成多重背包
模板代码略
恰填满背包的方案总数
- 对于一个给定了背包容量、物品费用、物品间相互关系(分组、依赖)的背包问题,除了再给定每个物品的价值后求可得到的最大价值外,还可以得到装满背包或将背包装至某一指定容量的方案总数
- 对于这类问题,一般只需要将状态转移方程中的max改成sum,再将边界稍作修改即可
基本方法
f[i][j]表示将前i件物品恰填满容量为j的背包的方案总数
- 01背包的状态转移方程是 f [ i ] [ j ] = f [ i − 1 ] [ j ] + f [ i − 1 ] [ j − v [ i ] ] ; f[i][j]=f[i-1][j]+f[i-1][j-v[i]]; f[i][j]=f[i−1][j]+f[i−1][j−v[i]];
- 完全背包的状态转移方程是 f [ i ] [ j ] = f [ i − 1 ] [ j ] + f [ i ] [ j − v [ i ] ] ; f[i][j]=f[i-1][j]+f[i][j-v[i]]; f[i][j]=f[i−1][j]+f[i][j−v[i]];
- 多重背包的状态转移方程是 f [ i ] [ j ] = ∑ k = 0 min ( c [ i ] , j / v [ i ] ) f [ i − k ⋅ v [ i ] ] ; f[i][j]=\sum_{k=0}^{\min(c[i],j/v[i])}f[i-k\cdot v[i]]; f[i][j]=∑k=0min(c[i],j/v[i])f[i−k⋅v[i]];
- 边界: f [ 0 ] [ 0 ] = 1 f[0][0]=1 f[0][0]=1
思考
- 在01背包达到最优解时,输出最优解中所有选中物品的编号
参考代码:
#include <iostream>
#include <cstdio>
#define MAXN 10010
#define MAXM 10010
//分开定义MAXN和MAXM表示这个数组的大小跟n和m中的哪一个有关
using namespace std;
int n,m,v[MAXN],w[MAXN],f[MAXM];
bool g[MAXN][MAXM];
void DFS(int x,int y){
if(x==0)
return;
if(g[x][y]){
DFS(x-1,y-v[x]);
printf("%d ",x);
}
else{
DFS(x-1,y);
}
}
int main(){
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d %d",&v[i],&w[i]);
for(int i=1;i<=n;i++){
for(int j=m;j>=v[i];j--){//降维
if(f[j-v[i]]+w[i]>f[j]){
f[j]=f[j-v[i]]+w[i];
g[i][j]=true;
}
}
}
printf("物品最大价值:%d\n选的物品:",f[m]);
DFS(n,m);//挨个输出物品
return 0;
}
恰填满背包的最小物品数
- 对于一个给定了背包容量、物品费用、物品间相互关系(分组、依赖)的背包问题,除了再给定每个物品的价值后求可得到的最大价值外,还可以得到装满背包或将背包装至某一指定容量的最小物品数量
基本方法
f[i][j]表示将前i件物品恰填满容量为j的背包的最小物品数
- 01背包的状态转移方程是 f [ i ] [ j ] = min ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − v [ i ] ] + 1 ) ; f[i][j]=\min(f[i-1][j],f[i-1][j-v[i]]+1); f[i][j]=min(f[i−1][j],f[i−1][j−v[i]]+1);
- 完全背包的状态转移方程是 f [ i ] [ j ] = min ( f [ i − 1 ] [ j ] , f [ i ] [ j − v [ i ] ] + 1 ) ; f[i][j]=\min(f[i-1][j],f[i][j-v[i]]+1); f[i][j]=min(f[i−1][j],f[i][j−v[i]]+1);
- 多重背包的状态转移方程是 f [ i ] [ j ] = min ( f [ i ] [ j ] , f [ i − 1 ] [ j − k × v [ i ] ] + k ) ; f[i][j]=\min(f[i][j],f[i-1][j-k \times v[i]]+k); f[i][j]=min(f[i][j],f[i−1][j−k×v[i]]+k);其中 0 ≤ k ≤ m i n ( c [ i ] , j / v [ i ] ) 0 \le k \le min(c[i],j/v[i]) 0≤k≤min(c[i],j/v[i])
- 边界: f [ 0 ] [ i ] = 0 x 3 f 3 f 3 f 3 f , f [ 0 ] [ 0 ] = 1 f[0][i]=0x3f3f3f3f,f[0][0]=1 f[0][i]=0x3f3f3f3f,f[0][0]=1