背包问题
01背包
问题描述
有N件物品和一个容量为V的背包, 每件物品只能用一次 \textcolor{red}{每件物品只能用一次} 每件物品只能用一次
第i件物品的体积为vi,价值为wi
求解将哪些物品装入背包,可以使这些物品的总体积不超过背包容量,且总价值最大。
输出该最大的总价值
问题分析
一般来说动态规划中规划的是可变的量,对于这个问题来讲
N和V是可变的,也就是说,我们是也可以求数量为N-1,体积为V-1的情况
所以我们可以建立一个二维动态数组dp[n][v],用来表示在数量为n,最大体积为v的情况下,最大的总价值
那么如何建立dp[n1][v1]到dp[n2][v2]的状态转移方程呢
我们对n1和v1逐渐递增
令n2=n1+1
设标号为n2的物品的体积为v,在v2>v的情况下
我们会有两种方案:选择第n2件物品和不选择第n2件物品
选择第n2件物品:dp[n2][v2]=dp[n1][v2-v]+w
不选择第n2件物品:dp[n2][v2]=dp[n1][v2]
因为我们要选择最大值,所以最终状态转移方程为:
d
p
[
n
2
]
[
v
2
]
=
m
a
x
(
d
p
[
n
1
]
[
v
2
−
v
]
+
w
,
d
p
[
n
1
]
[
v
2
]
)
dp[n2][v2]=max(dp[n1][v2-v]+w,dp[n1][v2])
dp[n2][v2]=max(dp[n1][v2−v]+w,dp[n1][v2])
所以我们的最大值就为dp[n][m]
原始代码
#include<bits/stdc++.h>
using namespace std;
int main(){
int N,V;
cin>>N;
cin>>V;
int dp[N+1][V+1];
int v[N+1];
int w[N+1];
for(int i=0;i<=N;i++){
for(int j=0;j<=V;j++){
dp[i][j]=0;
}
}
for(int i=1;i<=N;i++){
cin>>v[i];
cin>>w[i];
}
for(int i=1;i<=N;i++){
for(int j=1;j<=V;j++){
dp[i][j]=dp[i-1][j];
if(j>=v[i]){
dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
}
}
}
cout<<dp[N][V];
return 0;
}
时间复杂度:O(N*V)
空间复杂度:O(N*V)
空间优化过程
由状态转移方程
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]的状态只和dp[i-1]的状态有关
所以我们没有必要保存之前的状态
二维动态规划过程:
一维动态规划过程:
这里面有一个很重要的点,需要注意到在二维规划过程中 在更新 d p [ i ] [ j ] 所用到的 d p [ i − 1 ] [ j − v ] 和 d p [ i − 1 ] [ j ] 都是在 i − 1 中的 \textcolor{red}{在更新dp[i][j]所用到的dp[i-1][j-v]和dp[i-1][j]都是在i-1中的} 在更新dp[i][j]所用到的dp[i−1][j−v]和dp[i−1][j]都是在i−1中的所以在一维动态
规划中更新dp[j]时不能用已经更新过的dp[j-v],而要用未被更新过的dp[j-v],所以我们不能顺序更新j,因为在顺序执行下,在更新dp[j]时,dp[j-v]已经被更新过了,所以我们需要逆序更新j
空间优化代码
#include<bits/stdc++.h>
using namespace std;
int main(){
int N,V;
cin>>N;
cin>>V;
int dp[V+1];
int v[N+1];
int w[N+1];
for(int i=0;i<=V;i++){
dp[i]=0;
}
for(int i=1;i<=N;i++){
cin>>v[i];
cin>>w[i];
}
for(int i=1;i<=N;i++){
for(int j=V;j>=1;j--){
if(j>=v[i]){
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
}
}
}
cout<<dp[V];
return 0;
}
时间复杂度:O(N*V)
空间复杂度:O(V)
完全背包
题目描述
有N种物品和一个容量为V的背包, 每种物品都可以无限制的使用 \textcolor{red}{每种物品都可以无限制的使用} 每种物品都可以无限制的使用
第i种物品的体积是vi,价值是wi
求解将哪些物品装入背包,可使这些物品总体积不超过背包容量,且总价值最大
输出该最大价值
问题分析
完全背包问题和01背包问题的区别在于完全背包可以将物品使用无限次
所以我们的状态转移方程是
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
−
v
]
+
w
,
d
p
[
i
−
1
]
[
j
−
2
∗
v
]
+
2
∗
w
.
.
.
.
.
)
(
1
)
dp[i][j]=max(dp[i-1][j],dp[i-1][j-v]+w,dp[i-1][j-2*v]+2*w.....) (1)
dp[i][j]=max(dp[i−1][j],dp[i−1][j−v]+w,dp[i−1][j−2∗v]+2∗w.....)(1)
又因为
d
p
[
i
]
[
j
−
v
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
−
v
]
,
d
p
[
i
−
1
]
[
j
−
2
∗
v
]
+
w
,
d
p
[
i
−
1
]
[
j
−
3
∗
v
]
+
2
∗
w
.
.
.
.
.
.
)
(
2
)
dp[i][j-v]=max(dp[i-1][j-v],dp[i-1][j-2*v]+w,dp[i-1][j-3*v]+2*w......)(2)
dp[i][j−v]=max(dp[i−1][j−v],dp[i−1][j−2∗v]+w,dp[i−1][j−3∗v]+2∗w......)(2)
所以
d
p
[
i
]
[
j
−
v
]
+
w
=
m
a
x
(
d
p
[
i
−
1
]
[
j
−
v
]
+
w
,
d
p
[
i
−
1
]
[
j
−
2
∗
v
]
+
2
∗
w
,
d
p
[
i
−
1
]
[
j
−
3
∗
v
]
+
3
∗
w
.
.
.
.
.
.
)
(
3
)
dp[i][j-v]+w=max(dp[i-1][j-v]+w,dp[i-1][j-2*v]+2*w,dp[i-1][j-3*v]+3*w......)(3)
dp[i][j−v]+w=max(dp[i−1][j−v]+w,dp[i−1][j−2∗v]+2∗w,dp[i−1][j−3∗v]+3∗w......)(3)
将公式1和3连接起来得到
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
]
[
j
−
v
]
+
w
)
dp[i][j]=max(dp[i-1][j],dp[i][j-v]+w)
dp[i][j]=max(dp[i−1][j],dp[i][j−v]+w)
原始代码
#include<bits/stdc++.h>
using namespace std;
int main(){
int N,V;
cin>>N;
cin>>V;
int dp[N+1][V+1];
int v[N+1];
int w[N+1];
for(int i=0;i<=N;i++){
for(int j=0;j<=V;j++){
dp[i][j]=0;
}
}
for(int i=1;i<=N;i++){
cin>>v[i];
cin>>w[i];
}
for(int i=1;i<=N;i++){
for(int j=1;j<=V;j++){
dp[i][j]=dp[i-1][j];
if(j>=v[i]){
dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]]+w[i]);
}
}
}
cout<<dp[N][V];
return 0;
}
时间复杂度:O(N*V)
空间复杂度:O(N*V)
空间优化过程
看一下二维时是如何进行动态规划的
可以看出,与01背包最本质的区别在于用于更新dp[i][j]所用到的dp[i-1][j]和dp[i][j-v]是已经被更新过的了,所以按照和分析01背包的思路可以得出,应该对j顺序递增
空间优化代码
#include<bits/stdc++.h>
using namespace std;
int main(){
int N,V;
cin>>N;
cin>>V;
int dp[V+1];
int v[N+1];
int w[N+1];
for(int i=0;i<=V;i++){
dp[i]=0;
}
for(int i=1;i<=N;i++){
cin>>v[i];
cin>>w[i];
}
for(int i=1;i<=N;i++){
for(int j=1;j<=V;j++){
if(j>=v[i]){
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
}
}
}
cout<<dp[V];
return 0;
}
多重背包
问题描述
有N种物品和一个容量为V的背包
第i件物品最多有si件,每件的体积是vi,价值是wi
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大
输出该最大总价值
问题分析
多重背包和01背包的区别在于多重背包可以将同一个物品取s次
根据01背包的思路,可以得到状态转移方程如下
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
−
v
]
+
w
,
d
p
[
i
−
1
]
[
j
−
2
∗
v
]
+
2
∗
w
+
.
.
.
.
.
+
d
p
[
i
−
1
]
[
j
−
s
∗
v
]
+
s
∗
w
)
dp[i][j]=max(dp[i-1][j],dp[i-1][j-v]+w,dp[i-1][j-2*v]+2*w+.....+dp[i-1][j-s*v]+s*w)
dp[i][j]=max(dp[i−1][j],dp[i−1][j−v]+w,dp[i−1][j−2∗v]+2∗w+.....+dp[i−1][j−s∗v]+s∗w)
原始代码
#include<bits/stdc++.h>
using namespace std;
int main(){
int N,V;
cin>>N;
cin>>V;
int dp[N+1][V+1];
int v[N+1];
int w[N+1];
int s[N+1];
for(int i=0;i<=N;i++){
for(int j=0;j<=V;j++){
dp[i][j]=0;
}
}
for(int i=1;i<=N;i++){
cin>>v[i];
cin>>w[i];
cin>>s[i];
}
for(int i=1;i<=N;i++){
for(int j=1;j<=V;j++){
dp[i][j]=dp[i-1][j];
for(int k=0;k<=s[i];k++){
if(j>=k*v[i]){
dp[i][j]=max(dp[i][j],dp[i-1][j-k*v[i]]+k*w[i]);
}
}
}
}
cout<<dp[N][V];
return 0;
}
时间复杂度:O(N*V*K)
空间复杂度:O(N*V)
然后根据01背包的空间优化可以写出
空间优化代码
#include<bits/stdc++.h>
using namespace std;
int main(){
int N,V;
cin>>N;
cin>>V;
int dp[V+1];
int v[N+1];
int w[N+1];
int s[N+1];
for(int i=0;i<=V;i++){
dp[i]=0;
}
for(int i=1;i<=N;i++){
cin>>v[i];
cin>>w[i];
cin>>s[i];
}
for(int i=1;i<=N;i++){
for(int j=V;j>=1;j--){
for(int k=0;k<=s[i];k++){
if(j>=k*v[i]){
dp[j]=max(dp[j],dp[j-k*v[i]]+k*w[i]);
}
}
}
}
cout<<dp[V];
return 0;
}
时间复杂度:O(N*V*K)
空间复杂度:O(V)
二进制优化过程
首先我们考虑一个问题:
在选择k件物品的时候,除了顺序遍历[0,k]外,还有没有其他的方法
答案是二进制法
举个例子
假设我们要取1000件物品,我们可以将1000件物品提前分成1,2,4,8,16,32,64,128,256,512这10堆,然后我们只需要遍历这10堆,然后选与不选任意选择,就也可以达到顺序遍历[0,k]的目的(注意不要让最后的值大于1000)
二进制优化代码
#include<bits/stdc++.h>
using namespace std;
int main(){
int N,V;
cin>>N;
cin>>V;
int dp[V+1];
vector<int> v;
vector<int> w;
vector<int> s;
v.push_back(0);
w.push_back(0);
s.push_back(0);
for(int i=0;i<=V;i++){
dp[i]=0;
}
for(int i=1;i<=N;i++){
int a,b,c;
int f=1;
int sum=1;
int pre=0;
cin>>a;
cin>>b;
cin>>c;
while(sum<=c){
pre+=f;
v.push_back(a);
w.push_back(b);
s.push_back(f);
f=2*f;
sum+=f;
}
sum=sum-f;
int cnt=c-sum;
if(cnt>0){
v.push_back(a);
w.push_back(b);
s.push_back(cnt);
}
}
N=v.size();
for(int i=1;i<=N;i++){
for(int j=V;j>=1;j--){
if(j>=v[i]*s[i]){
dp[j]=max(dp[j],dp[j-s[i]*v[i]]+s[i]*w[i]);
}
}
}
cout<<dp[V];
return 0;
}
时间复杂度O(N*V*log(k))
空间度杂度O(V)
单调队列优化过程
根据原始的一维状态转移方程可以求得
d
p
[
j
]
=
d
p
[
j
]
dp[j]=dp[j]
dp[j]=dp[j]
d p [ j + v ] = m a x ( d p [ j + v ] , d p [ j ] ) dp[j+v]=max(dp[j+v],dp[j]) dp[j+v]=max(dp[j+v],dp[j])
d p [ j + 2 ∗ v ] = m a x ( d p [ j + 2 ∗ v ] , d p [ j + v ] + w , d p [ j ] + 2 ∗ w ) dp[j+2*v]=max(dp[j+2*v],dp[j+v]+w,dp[j]+2*w) dp[j+2∗v]=max(dp[j+2∗v],dp[j+v]+w,dp[j]+2∗w)
d p [ j + 3 ∗ v ] = m a x ( d p [ j + 3 ∗ v ] , d p [ j + 2 ∗ v ] + w , d p [ j + v ] + 2 ∗ w , d p [ j ] + 3 ∗ w ) dp[j+3*v]=max(dp[j+3*v],dp[j+2*v]+w,dp[j+v]+2*w,dp[j]+3*w) dp[j+3∗v]=max(dp[j+3∗v],dp[j+2∗v]+w,dp[j+v]+2∗w,dp[j]+3∗w)
. . .
. . .
. . .
d p [ j + s ∗ v ] = m a x ( d p [ j + s ∗ v ] + . . . . + d p [ j ] + s ∗ w ) dp[j+s*v]=max(dp[j+s*v]+....+dp[j]+s*w) dp[j+s∗v]=max(dp[j+s∗v]+....+dp[j]+s∗w)
使变量统一,给每一个方程左右两边减去i*w得到
d
p
[
j
]
=
d
p
[
j
]
dp[j]=dp[j]
dp[j]=dp[j]
d p [ j + v ] − w = m a x ( d p [ j ] , d p [ j + v ] − w ) dp[j+v]-w=max(dp[j],dp[j+v]-w) dp[j+v]−w=max(dp[j],dp[j+v]−w)
d p [ j + 2 ∗ v ] − 2 ∗ w = m a x ( d p [ j ] , d p [ j + v ] − w , d p [ j + 2 ∗ v ] − 2 ∗ w ) dp[j+2*v]-2*w=max(dp[j],dp[j+v]-w,dp[j+2*v]-2*w) dp[j+2∗v]−2∗w=max(dp[j],dp[j+v]−w,dp[j+2∗v]−2∗w)
d p [ j + 3 ∗ v ] − 3 ∗ w = m a x ( d p [ j ] , d p [ j + v ] − v , d p [ j + 2 ∗ v ] − 2 ∗ w , d p [ j + 3 ∗ v ] − 3 ∗ w ) dp[j+3*v]-3*w=max(dp[j],dp[j+v]-v,dp[j+2*v]-2*w,dp[j+3*v]-3*w) dp[j+3∗v]−3∗w=max(dp[j],dp[j+v]−v,dp[j+2∗v]−2∗w,dp[j+3∗v]−3∗w)
. . .
. . .
. . .
d p [ j + s ∗ v ] − s ∗ w = m a x ( d p [ j ] , . . . . . . d p [ j + s ∗ v ] − s ∗ w ) dp[j+s*v]-s*w=max(dp[j],......dp[j+s*v]-s*w) dp[j+s∗v]−s∗w=max(dp[j],......dp[j+s∗v]−s∗w)
所以可以根据余数j将[0,k]划分为x个单调队列,然后求出每个队列的最大值即可
注意每次入队的值为 j + k ∗ v j+k*v j+k∗v,从队尾弹出元素的判断条件是 p r e [ k ] − ( k − j ) / v ∗ w > p r e [ q [ t ] ] − ( q [ t ] − j ) / v ∗ w pre[k]-(k-j)/v*w>pre[q[t]]-(q[t]-j)/v*w pre[k]−(k−j)/v∗w>pre[q[t]]−(q[t]−j)/v∗w
p r e pre pre数组是当前步骤还未被更新的数组
单调队列优化代码
#include<bits/stdc++.h>
using namespace std;
int main(){
int N,V;
scanf("%d %d",&N,&V);
vector<int> dp(V+1);
vector<int> q(V+1);
for(int i=1;i<=N;i++){
int v,w,s;
scanf("%d %d %d",&v,&w,&s);
vector<int> pre=dp;
for(int j=0;j<v;j++){
int h=0,t=-1;
for(int k=j;k<=V;k+=v){
if(t>=h&&k-q[h]>s*v){
h++;
}
if(t>=h){
dp[k]=max(dp[k],pre[q[h]]+(k-q[h])/v*w);
}
while(t>=h&&pre[q[t]]-(q[t]-j)/v*w < pre[k]-(k-j)/v*w){
t--;
}
q[++t]=k;
}
}
}
cout<<dp[V];
return 0;
}