前置知识
多重背包 (背包九讲)
背包九讲——全篇详细理解与代码实现_良月澪二的博客-优快云博客_背包九讲
单调队列
前言
笔者学习这一算法耗了很多精力,网上多重背包讲解多如牛毛,单调队列优化讲的也不少,但大都是蜻蜓点水罢了,很不详细。在此决定自己亲自写一篇,也是对知识点的梳理。
首先,对于常规多重背包,f[i][j]代表体积为j,选到第i个物品的最大价值。由于一种物体数量可以多个,故需要枚举数量。
状态转移方程如下
f[i][j]=max( f[i-1] [j-k*v[i]] +k*w[i] ) 其中0<=k<=cnt[i]
cnt[i]代表这一种物体数目,w[i]是价值(wealth), v[i]是体积
我们设 j=k1*v[i] +d k1=j/v[i] d=j%v[i] 前者是j能装下的最大个数,后者是余数
下面建议拿本子推一下
f[i][j]= max( f[i-1][ k1*v[i]+d -k*v[i]] + k*w[i] ) 0<=k<=cnt[i]
没做太多变化,把j代换成了 k1*w[i] +d
接下来,合并同类项
f[i][j]= max( f[i-1][ (k1-k)* v[i]+d] -(k1 -k) *w[i] +k1*w[i] ) 0<=k<=cnt[i]
把k和k1提取出来了,另外把 k*w[i] 改写成 (k1 -k) *w[i] +k1*w[i]
f[i][j]= max( f[i-1][ (k1-k)* v[i]+d] -(k1 -k) *w[i] ) + k1*w[i] 0<=k<=cnt[i]
对于确定的d,j,k1是常数,故提取出外面
为了更直观看出这样转化的原理,看下面例子
f[i][ 6v+d ] = max( f[i-1][6v+d] ,f[i-1][5v+d] + w ,f[i-1][4v+d] +2w , f[i-1][3v+d] +3w.....)
应该没什么毛病,对于确定的j=6v+d,往里面塞0个,1个,2个..的价值
然后,我们对max括号里每一项,全部减去 6w ,6指的是当前j能装下的最多数目
f[i][ 6v+d ] = max( f[i-1][6v+d]-6w ,f[i-1][5v+d] -5w ,f[i-1][4v+d] -4w , f[i-1][3v+d] -3w....) +6*w
同时减去相同数字,大小关系不变。
我们惊奇发现,这不就是上面的式子吗?k1=6 ,(k1-k)分别是那些5,4,3,那些系数
f[i][j]= max( f[i-1][ (k1-k)* v[i]+d] -(k1 -k) *w[i] ) + k1*w[i] 0<=k<=cnt[i]
我们继续来看下面这个式子
f[i][ 6v+d ] = max( f[i-1][6v+d]-6w ,f[i-1][5v+d] -5w ,f[i-1][4v+d] -4w , f[i-1][3v+d] -3w....) +6*w
它就告诉了我们,想知道左边的,必须求出右边括号里的,然后再加上6*w,你可能会说,这不显然吗?
哎别,如果我们一个一个·枚举,效率不知道会低多少,这就引出了我们的杀手锏--单调队列
它的精华在于维护一个区间(窗口)的一个最值。
那么这个公式和窗口有什么关系呢?
嘿嘿,看清楚了,别眨眼,我们把(k1-k)用k,代换 ,修改k的定义域
原来是0<=k<=cnt[i] ,现在 k1-cnt[i]<= k <= k1 。这时候我们惊奇的发现,当j变化时,k1随之变化,k值也随之变化,但k值始终在一个窗口里!!
所以,我们对一个j,用一个单调队列维护窗口内的一些值,那些值?
接着看那个式子
f[i][j]= max( f[i-1][ (k1-k)* v[i]+d] -(k1 -k) *w[i] ) + k1*w[i] 0<=k<=cnt[i]
(k1-k)-->k
f[i][j]= max( f[i-1][ k* v[i]+d] -k *w[i] ) + k1*w[i] k1-cnt[i]<= k <=k1
我们维护窗口内, f[i-1][k*v[i]+d] -k*w[i]
到达我们滑动到目标j,的时候,区间最值已经出来了,f[i][j]就等于那个最值 + k1*w[i]
大功告成!!
但我们还不满足,我们不满足于二维,我们把它滚成一维,如果对这方面还不熟悉,可以参考背包九讲。
首先,对于新输入的一个物品,w,v,c代表价值,体积,数目,已知最大体积V,那么它最多能容纳V/v 个,
int v,w,c;
cin>>v>>w>>c;
int k=V/v;
c=min(c,k);
c可能大于最大容量,所以取最小值.
我们枚举余数d, 对于每个体积,它模v,余数在0---v-1范围内
for(int d=0;d<v;d++)
然后对于一个确定的d,j最多能容纳
k=(V-d)/v;
枚举k,之前别忘了初始化单调队列
每次加入单调队列的,是dp[d+j*v]-j*w , 这样,在计算每个目标点时,只需要提取出窗口最大值,再加上特定的k1*w即可
for(int d=0;d<v;d++){
head=tail=0;
k=(V-d)/v;
for(int j=0;j<=k;j++){
while(head<tail&&dp[d+j*v]-j*w>=q2[tail-1])
tail--;
q[tail]=j;
q2[tail++]=dp[d+j*v]-j*w;
while(head<tail&&q[head]<j-c)
++head;
dp[d+j*v]=max(dp[d+j*v],q2[head]+j*w);
}
}
}