今天写一篇关于经典系列问题——背包问题中的多重背包问题的博客,在其中我将会提到三种多重背包的解法,分别为朴素算法,二进制拆分和单调队列优化,讲解重点是第三个,单调队列优化多重背包。
多重背包问题一般模型:一个背包的体积为 V V V,有 n n n 种物品,每种物品有 S i S_i Si 件,每种物品的体积和价值分别为 v i v_i vi 和 w i w_i wi。问:如何装可以使背包内物品总价值最大,输出最大价值。
解法一:(数据范围 0 < N , V ≤ 100 , 0 < v i , w i , s i ≤ 100 0
直接参照背包最一般的循环解法即可,即先循环物品,再循环体积,最后进行决策,不进行任何优化。
解法二:(数据范围 0 < N ≤ 1000 , 0 < V ≤ 2000 , 0 < v i , w i , s i ≤ 2000 0
二进制拆分的核心思想:将每个 s i s_i si 用 2 2 2 的整数次幂来表示,然后每一个 2 2 2 的整数次幂“份”物体(剩下的也是“一份”)看作一个01背包的物品,因为01背包中对这些“物品”的取与不取的决策组合可以满足 0 0 0~ s i s_i si 内的任何整数,即第 i i i 件物品的任何合法数量都可以取到。
代码如下:
#include
#include
#include
using namespace std;
const int N=25000,M=2010;
int w[N],v[N],f[N*M],n,V,cnt;
int main(){
int cnt=0;
cin>>n>>V;
for(int i=1;i<=n;i++){
int a,b,s;
cin>>a>>b>>s;
int k=1;
while(k<=s){
cnt++;
w[cnt]=k*b;
v[cnt]=k*a;
s-=k;
k*=2;
}
if(s>0){
cnt++;
w[cnt]=s*b;
v[cnt]=s*a;
}
}
n=cnt;
for(int i=1;i<=n;i++)
for(int j=V;j>=v[i];j--){
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
cout<<f[V]<<endl;
return 0;
}
解法三:(数据范围: 0 < N ≤ 1000 0
先给出代码:
#include
#include
#include
#include
using namespace std;
const int N=20010;
int f[N],g[N],q[N],n,V;
int main(){
cin>>n>>V;
for(int i=1;i<=n;i++){
int v,w,s;
cin>>v>>w>>s;
memcpy(g,f,sizeof(f));
for(int j=0;j<v;j++){ //mod v 的余数 *
int hh=0,tt=-1;
for(int k=j;k<=V;k+=v){ //单调队列里存储的相当于是使g[体积]单调下降的体积,因为一般情况体积剩的越多,g[]越小
if(hh<=tt && q[hh]<k-s*v) hh++;
while(hh<=tt && g[q[tt]]-q[tt]/v*w<g[k]-k/v*w) tt--;
q[++tt]=k;
f[k]=g[q[hh]]+(k-q[hh])/v*w; //*
}
}
}
cout<<f[V]<<endl;
return 0;
}
首先代码整体仍然是按照背包一般转移方法来写的,即最外层循环物品,次外层循环体积,内层循环决策。这里最外层循环物品没有变化,而次内层和内层循环是通过单调队列(求滑动窗口内最大(小)值)实现的。(注:此处为省去 f f f 一维数组,用了数组 g g g 来记录上一层的数据)
在这之后,循环体积时,为了可以更新到背包所有的可用空间同时又可以高效的求出背包在某个体积时的最大值,以 0 0 0~ v i − 1 v_i-1 vi−1 内每一个整数作为起点,累加 v i v_i vi,同时又不能选择超过 s i s_i si 个物品 i i i ,所以问题等价于求一个长度为 s i s_i si 的滑动窗口内的最大值,这个过程是可以在线性的时间复杂度内维护的。
最后,决策则不需要循环,只需要取出队头元素进行合法的状态转移即可。
单调队列在DP优化中有着很多应用,之后应该也会更新关于此的题目。(单调队列的模板也会尽快补上)