背包问题是典型的动态规划问题,本文将对典型的背包问题进行总结。
0-1背包问题
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出最大价值。
输入格式 第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式 输出一个整数,表示最大价值。
「0-1 背包」即是不断对第 i 个物品的做出决策,「0-1」正好代表不选与选两种决定。
用
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示前i个物品,背包容量j下的最优解,那么有递推公式
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
−
v
[
i
]
]
+
w
[
i
]
)
dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]]+w[i])
dp[i][j]=max(dp[i−1][j],dp[i−1][j−v[i]]+w[i])
滚动数组优化=>更新至一维
观察上面代码中的循环体。你会发现dp[i][j]永远和dp[i-1]有关系,也就是说该层数据是由上一层数据得到的。那么我们就可以不用存储之前的数据只用存储上一层数据通过遍历就可以推导出最终结果。
在上一次计算出
d
p
[
i
−
1
]
[
j
]
dp[i-1][j]
dp[i−1][j],
d
p
[
i
−
1
]
[
j
−
v
[
i
]
]
dp[i-1][j-v[i]]
dp[i−1][j−v[i]]后,我们只要保证在计算
d
p
[
j
]
dp[j]
dp[j]前对
d
p
[
j
]
dp[j]
dp[j],
d
p
[
j
−
v
[
i
]
]
dp[j-v[i]]
dp[j−v[i]]没有进行更新,那自然是第
i
−
1
i-1
i−1次的值。那如何保证呢?从后向前遍历
因此有递推公式
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])
完整代码如下
#include<iostream>
using namespace std;
const int N = 10010;
int n, m;
int f[N];
int main(){
cin >> n >>m;
for(int i=1; i<=n; ++i){
int v, w;
cin >> v >> w;
for(int j=m; j>=v; --j){
f[j] = max(f[j], f[j-v]+w);
}
}
cout << f[m] << endl;
}
完全背包问题
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出最大价值。
输入格式 第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。
输出格式 输出一个整数,表示最大价值。
与0-1背包问题有区别的是,0-1背包问题中每个背包只能选择一次,而这里每个背包可以选择多次。
用
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示对前
i
i
i个背包有容量
j
j
j,那么递推公式有
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
]
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
]
[
j
]
,
d
p
[
i
]
[
j
−
v
[
k
]
]
+
1
)
dp[i][j] = dp[i-1][j]\\ dp[i][j] = max(dp[i][j], dp[i][j-v[k]]+1)
dp[i][j]=dp[i−1][j]dp[i][j]=max(dp[i][j],dp[i][j−v[k]]+1)
核心代码如下
for(int i=1; i<=n; ++i)
for(int j=0; j<=m; ++j){
dp[i][j] = dp[i-1][j];
if(j-v[i]>=0){
dp[i][j] = max(dp[i][j], dp[i][j-v[i]]+w[i]);
}
}
类似0-1背包问题的优化方式, d p [ i ] [ j ] dp[i][j] dp[i][j]是依赖它前面的 d p [ i ] [ j − v [ i ] dp[i][j-v[i] dp[i][j−v[i]的值,与i无关,代码可以改写成为
for(int i=1; i<=n; ++i)
for(int j=v[i]; j<=m; ++j){
dp[j] = max(dp[j], dp[j-v[i]]+w[i]);
}
最终,代码可以写为
#include<iostream>
using namespace std;
const int N = 10010;
int f[N];
int main(){
int n, m;
cin >> n >> m;
for(int i=1; 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;
}
多重背包
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出最大价值。
输入格式 第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式 输出一个整数,表示最大价值。
多重背包问题和前两种背包问题的区别是:多重背包问题每类物品的数量是有限的。
那一个直接的思路是把多重背包问题直接转化为01背包问题。
#include <iostream>
using namespace std;
int a[10005],b[10005];
int n, m;
int dp[10005];
int main()
{
int t = 0;
cin>>n>>m;
while(n--)
{
int v, w, s;
cin>>v>>w>>s;
while(s--)
{
a[++t]=v;
b[t]=w;
}//把多重背包拆成01背包
}
for(int i=1;i<=t;i++)
for(int j=m;j>=a[i];j--)
dp[j]=max(dp[j-a[i]]+b[i],dp[j]);//01背包
cout<<dp[m]<<endl;
return 0;
}
但是当我们个数太多时,一个一个加是非常慢的。这里可以采用二进制优化。
比如我们现在有
n
n
n个物品,刚刚我们的思路是从第1个物品到第n个物品,依次去问要不要取?但是我们的问题不是每个物品要不要取,而且一共取几个物品。
那我们可以用第1位代表第1堆有1个,用第2位代表第2堆有2个,用第k位代表第k堆有
2
k
2^k
2k个。这样长度为n的数最大为用
l
o
g
(
n
)
+
1
log(n)+1
log(n)+1的位数就可以表示。那操作步骤就可以为:先去问第1堆要不要把第一堆放进包中,然后去问第二堆,这样继续…
#include<iostream>
using namespace std;
const int N = 12100, M = 2020;
int n, m;
int v[N], w[N]; //枚举的总堆数
int dp[M]; //体积
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;
}
}
for(int i=1; i<=cnt; ++i)
for(int j=m; j>=v[i]; --j) dp[j] = max(dp[j], dp[j-v[i]]+w[i]);
cout << dp[m] << endl;
return 0;
}