概览检索
动态规划DP 概览(点击链接跳转)
动态规划DP 背包问题 概览(点击链接跳转)
多重背包问题1
原题链接
AcWiing 4. 多重背包问题1
题目描述
有 N种物品和一个容量是 V的背包。
第 i 种物品最多有 si件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤100
0<vi,wi,si≤100
输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例:
10
题目分析
多重背包:对于每个物品可以选择0,1,2,···,s[i]个(有限个)。
闫氏DP分析法
f
[
i
,
j
]
f[i,j]
f[i,j] 表示从1~i 个物品中选择且总体积不超过 j 的选法中价值最大的那个。
对于第i个物品,
可以选0个 i,即在 0~i -1 个物品中选择,体积不超过j,即
f
[
i
−
1
,
j
]
f[i-1,j]
f[i−1,j]。
可以选1个 i,同时在 0~i -1 个物品中选择,体积不超过
j
−
v
[
i
]
j-v[i]
j−v[i],即
f
[
i
−
1
,
j
−
v
[
i
]
]
+
w
[
i
]
f[i-1,j-v[i]]+w[i]
f[i−1,j−v[i]]+w[i]。
···
可以选 s[i] 个 i,同时在 0~i -1 个物品中选择,体积不超过
j
−
s
[
i
]
×
v
[
i
]
j-s[i] \times v[i]
j−s[i]×v[i],即
f
[
i
−
1
,
j
−
s
[
i
]
×
v
[
i
]
]
+
s
[
i
]
×
w
[
i
]
f[i-1,j-s[i] \times v[i]]+s[i] \times w[i]
f[i−1,j−s[i]×v[i]]+s[i]×w[i]。
其中,选择物品i 的个数由限定有限的s[i]和背包体积共同限定。
综上,
f
[
i
,
j
]
f[i,j]
f[i,j] 即为上述所有结果的最大值。
f
[
i
,
j
]
=
m
a
x
(
f
[
i
−
1
,
j
]
,
f
[
i
−
1
,
j
−
k
×
v
[
i
]
]
+
k
×
w
[
i
]
)
f[i,j]=max(f[i-1,j],f[i-1,j-k \times v[i]]+k \times w[i])
f[i,j]=max(f[i−1,j],f[i−1,j−k×v[i]]+k×w[i])
完整代码
#include <iostream>
#include <algorithm>
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];
//遍历n个物品
for(int i=1;i<=n;i++){
//遍历背包体积
for(int j=0;j<=m;j++){
//遍历物品i的个数
for(int k=0;k<=j/v[i]&&k<=s[i];k++){
f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
}
}
}
cout<<f[n][m]<<endl;
return 0;
}
多重背包问题2
原题链接
AcWing 5.多重背包问题2
题目分析
与上述完全背包1的描述完全相同,不同点在于数据范围的变化。
数据范围
0<N≤1000
0<V≤2000
0<vi,wi,si≤2000
数据范围变大,由原来的100变为1000/2000,需对原先代码进行优化,采用 二进制优化。
在原先的代码中,我们枚举了0~s[i]的所有选法,
是否存在其他的办法让我们不用一项一项枚举呢?
采用 打包 。
情况1:s[i]恰为 2 k − 1 2^{k}-1 2k−1
例如,s[i]=127,
原来我们需枚举:1,2,3,···,127,127,共s=127个。
优化后,我们采用打包,打包为1个,2个,4个,8个,···,64个(2的次方),各位一组,共7组。
这相当于7组物品,每组物品可以选择 用 or 不用,相当于转化为 01背包问题 。
复杂度由
s
s
s 转化为
log
s
\log_{}s
logs
而这些打包数能否组合表示从1~127的所有数呢?
显然是可以的,
1,2可以组合成1~3的任意数;
1,2,4可以组合成1~7的任意数;
1,2,4,8可以组合成1~15的任意数;
···
1,2,4,···,64恰好可以组合成1~127的任意数。
因此,上述打包数可以组合表示1~127的所有数。
示意图如下:
情况2:s[i]不为 2 k − 1 2^{k}-1 2k−1
例如,s[i]=120,
原来我们需枚举:1,2,3,···,64,···,120,共s=120个。
优化后,我们采用打包,打包为1个,2个,4个,8个,···,32个,
(
120
−
1
−
2
−
4
−
⋅
⋅
⋅
−
32
)
=
57
(120-1-2-4-···-32)=57
(120−1−2−4−⋅⋅⋅−32)=57个,各为一组,共7组。
这相当于7组物品,每组物品可以选择 用 or 不用,相当于转化为 01背包问题 。
复杂度由
s
s
s 转化为
log
s
\log_{}s
logs
而这些打包数能否组合表示从1~120的所有数呢?
显然是可以的,
1,2可以组合成1~3的任意数;
1,2,4可以组合成1~7的任意数;
···
1,2,4,···,32恰好可以组合成1~63的任意数。
加上57,则可以组合成1+57 ~ 63+57即 58 ~ 120 的任意数。
拼接起来,
因此,上述打包数可以组合表示1~120的所有数。
综上,
对于任意的s,
打包为
1
,
2
,
4
,
8
,
⋅
⋅
⋅
,
2
k
,
c
1,2,4,8,···,2^{k},c
1,2,4,8,⋅⋅⋅,2k,c ,其中
c
<
2
k
+
1
c<2^{k+1}
c<2k+1 ,
1
,
2
,
4
,
8
,
⋅
⋅
⋅
,
2
k
1,2,4,8,···,2^{k}
1,2,4,8,⋅⋅⋅,2k可以凑出
0
∼
2
k
+
1
−
1
0 \thicksim2^{k+1}-1
0∼2k+1−1 的所有数,
加上c,则可以凑出
c
∼
2
k
+
1
−
1
+
c
c\thicksim2^{k+1}-1+c
c∼2k+1−1+c 的所有数,
又由于
c
<
2
k
+
1
c<2^{k+1}
c<2k+1 ,
即凑出了
0
∼
2
k
+
1
−
1
+
c
0 \thicksim2^{k+1}-1+c
0∼2k+1−1+c 的所有数,
使
2
k
+
1
−
1
+
c
=
s
2^{k+1}-1+c=s
2k+1−1+c=s 即可凑出
0
∼
s
0 \thicksim s
0∼s 的所有数。
自此打包为
log
s
\log_{}s
logs 组物品,转化为 01 背包问题。
完整代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N=20000,M=2010;
int v[N],w[N];
int f[N];
int n,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;
//2^k的打包操作
while(k<=s){
cnt++;
v[cnt]=k*a; //k个总体积
w[cnt]=k*b; //k个总价值
s-=k; //总数-k个
k*=2; //k更新为2*k
}
//如果总数量s不等于2^k-1,则剩余的打包为一组
//剩余s个
if(s>0){
cnt++;
v[cnt]=s*a; //剩余s个总体积
w[cnt]=s*b; //剩余s个总价值
}
}
//总组数为cnt
n=cnt;
//转化为cnt个01背包问题
//遍历n组物品
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]<<endl;
return 0;
}
多重背包问题3
原题链接
AcWing 6. 多重背包问题3
题目描述
与上述完全背包1的描述完全相同,不同点在于数据范围的变化。
数据范围
0<N≤1000
0<V≤20000
0<vi,wi,si≤20000
数据范围的再次扩大,需要对代码进行再次的优化。采用单调队列/滑动窗口
观察状态计算公式,
f[i,j]与f[i,j-v]有相同项,但f[i,j-v]比f[i,j]多了一项,仔细观察发现,f[i,j]的max计算即为f[i,j-v]的前s项+w,即 f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i , j − v ] 的前 s 项 + w ) f[i,j]=max(f[i-1,j],f[i,j-v]的前s项+w) f[i,j]=max(f[i−1,j],f[i,j−v]的前s项+w) 。
且对于每个 f [ i , j ] , f [ i , j − v ] , f [ i , j − 2 v ] , ⋅ ⋅ ⋅ f[i,j],f[i,j-v],f[i,j-2v],··· f[i,j],f[i,j−v],f[i,j−2v],⋅⋅⋅ 都可通过其前一项的前s项进行递推运算。
画出数轴对应示意图如下,每次计算出前s项中的最大值。
由此观察我们可以发现,与 滑动窗口 及其相似!
滑动窗口主要利用单调队列解决了一组数中滑动的固定大小的窗口中的最大值/最小值。
下面详细说明 滑动窗口 的思想。
滑动窗口/单调队列
原题链接
AcWing 154. 滑动窗口
题目描述
给定一个数组。
有一个大小为 k的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到 k个数字。
每次滑动窗口向右移动一个位置。
以下是一个例子:
该数组为 [1 3 -1 -3 5 3 6 7],k为 3。
窗口位置 | 最小值 | 最大值 |
---|---|---|
[1 3 -1] -3 5 3 6 7 | -1 | 3 |
1 [3 -1 -3] 5 3 6 7 | -3 | 3 |
1 3 [-1 -3 5] 3 6 7 | -3 | 5 |
1 3 -1 [-3 5 3] 6 7 | -3 | 5 |
1 3 -1 -3 [5 3 6] 7 | 3 | 6 |
1 3 -1 -3 5 [3 6 7] | 3 | 7 |
任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
题目分析
窗口可用队列来维护,队尾插入,队头弹出。
若求最小值,
如下列实例滑动窗口中,3<-3,-1<-3,且-3在后面,则只要-3在,则最小值一定是-3,3与-1的值一定不会被取到,
因此,提炼出,只要出现逆序对,即前面的数大于后面的数,则一定不会取到,则出队。
由此,可知,队列中的数一定是单调递增的( 单调队列 )。
则,队头存储的即为当前的最小值。
(可仔细看下方实例加深理解!!!)
求最大值同理。
完整代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N=1000010;
int n,k;
int a[N]; //存储原数组
int q[N]; //数组模拟队列,存储数组下标
int main(){
scanf("%d%d",&n,&k); //n为数组长度,k为滑动窗口长度
for(int i=0;i<n;i++) scanf("%d",&a[i]);
int hh=0,tt=-1;
//最小值
for(int i=0;i<n;i++){
//hh<=tt表明队列不为空
//判断队头是否已经滑出窗口
//窗口最前端的数组下标大于队首,则队头数出列
if(hh<=tt&&i-k+1>q[hh]) hh++; //i-k+1(当前数组下标i-窗口长度k+1)为当前窗口最前端的数组下标
//判断队尾数组是否大于当前数,若大于,出现逆序,则队尾的数弹出tt--
while(hh<=tt&&a[q[tt]]>=a[i]) tt--;
q[++tt]=i; //当前数入队
//前k-1个数时,不输出答案
if(i>=k-1) printf("%d ",a[q[hh]]);
}
puts("");
//最大值
hh=0,tt=-1;
for(int i=0;i<n;i++){
//判断队头是否已经滑出窗口
//窗口最前端的数组下标大于队首,则队头数出列
if(hh<=tt&&i-k+1>q[hh]) hh++; //i-k+1(当前数组下标i-窗口长度k+1)为当前窗口最前端的数组下标
//判断队尾数组是否小于当前数,若小于,出现逆序,则队尾的数弹出tt--
while(hh<=tt&&a[q[tt]]<=a[i]) tt--;
q[++tt]=i; //当前数入队
//前k-1个数时,不输出答案
if(i>=k-1) printf("%d ",a[q[hh]]);
}
}
回到原来的多重背包问题,借用单调队列/滑动窗口的思想。
完整代码
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N=20010;
int n,m;
int f[N]; //状态计算
int g[N]; //动态数组,动态存储前一个状态
int q[N]; //数组模拟队列
int main(){
cin>>n>>m;
for(int i=0;i<n;i++){
int v,w,s;
cin>>v>>w>>s; //体积,价值,数量
//将上次计算的f赋给g
memcpy(g,f,sizeof f);
//遍历体积
for(int j=0;j<v;j++){
int hh=0,tt=-1; //队头hh,队尾tt
//滑动的一组数,j,j+v,j+2v···,每次右滑v,窗口长度为s*v
for(int k=j;k<=m;k+=v){
//判断队头是否已滑出窗口
if(hh<=tt&&q[hh]<k-s*v) hh++; //当前数k-窗口长度s*v =滑动窗口左端的数组下标
//状态计算 f[i]=max(f[i],f[i-1]+相差的w)
if(hh<=tt) f[k]=max(f[k],g[q[hh]]+(k-q[hh])/v*w); //当前数下标k-队头存储数组下标可得前后状态二者相差的w的个数
//判断当前数是否大于队尾
//队尾的状态计算值为g[q[tt]],窗口中为不带w的计算,因此要减去对应数量的w
//(w数量的计算为当前队尾存储的下标q[tt]-最前端下标j)除以v,即为二者相差的距离,就是对应相差的w个数
//当前数的状态计算值为g[k],窗口中为不带w的计算,因此要减去对应数量的w
//(w数量的计算为当前数的下标k-最前端下标j)除以v,即为二者相差的距离,就是对应相差的w的个数
while(hh<=tt&&g[q[tt]]-(q[tt]-j)/v*w<=g[k]-(k-j)/v*w) tt--;
//当前数入队
q[++tt]=k;
}
}
}
cout<<f[m]<<endl;
return 0;
}