单调队列用于维护一个长度固定的区间内,数组的最值。以最大值为例,如果一个数组长度为n,取长度为m的区间,那么单调队列的队首一定是数组在该区间内的最大值。之所以是维护,就是当区间开始整体后移时,最大值可能发生变化,而单调队列可以在O(n)的时间复杂度下得到全部n-m+1个区间的最大值。
有N种物品,其数量用数组q[i]描述,每种物品的价值用数组v[i]描述,每种物品的重量用w[i]描述,现在有一个总承重为C的背包,如何放置物品可以使得背包内物品价值最大,而又不超过背包承重?
参考01背包和完全背包,使用DP的分析方法来看,对于第i种物品来说,容量为C的背包最多可以放下lim=min{C/w[i],q[i]}件i物品,如果用f[i][C]函数来描述对于前i种物品,容量为C的背包所能得到的最高价值,那么在已知f[i-1][0,1,2,…C]的情况下,实际上f[i][C]=max{f[i-1][C-kw[i]]+kv[i]}, 在0<=k<=lim的条件下。但在该状态转移方程中,等式左边只求出来背包大小为C时的最大价值,但是等式左边却需要很多背包容量小于等于C时,前i-1种物品的最大价值,这显然是不可持续的状态方程。因为如果使用该状态方程,那么在求i+1种物品的情况时,所需要的子问题解并没有被求出,因此是不可持续的。
现在的问题是需要在求f[i][C]的过程中,将f[i][0,1,2,3,…,C]也求出来。上一个递推公式中的k表示放入k件i物品,那么从补集的角度来看,我们可以选择从最多可放i物品的数量中扣除s件,用这种方式来表达最大价值。对于容量为C的背包来说,不管q[i]的限制,该背包最多可以放入C/w[i]件i物品,我们记为a,那么该背包可能会有剩余空间放不下i物品,此时剩余空间我们用b来表示,那么b=C%w[i]。如果我们一开始将a件i物品放入背包,之后从中扣除s件i物品,那么等价于放入a-s件i物品,即k=a-s,将其代入f[i][C]=max{f[i-1][C-kw[i]]+kv[i]}, 在0<=k<=lim,得到f[i][C]=max{f[i-1][sw[i]+b]+(a-s)*v[i]}, 在a-lim<=s<=a的条件下。此时对于背包容量固定的情况,a是定值,将其提出来,变为:
f [ i ] [ C ] = m a x ( f [ i − 1 ] [ s ∗ w [ i ] + b ] − s ∗ v [ i ] ) + a ∗ v [ i ] f[i][C]=max(f[i-1][s*w[i]+b]-s*v[i])+a*v[i] f[i][C]=max(f[i−1][s∗w[i]+b]−s∗v[i])+a∗v[i], 在a-lim<=s<=a的情况下。此时对该式子的理解为:从背包中扣除s件i物品,扣除下来的空间为sw[i]+b,用来放前i-1种物品,随之也要减去因为扣除s件i物品的价值损失,因此减去sv[i],扣除之后再将a件i物品的价值补充进入背包,里外里等价于放入a-s件i物品,并且剩下的空间用来放前i-1种物品。因为a是定值,所以我们可以先求 f [ i ] [ C ] = m a x ( f [ i − 1 ] [ s ∗ w [ i ] + b ] − s ∗ v [ i ] ) f[i][C]=max(f[i-1][s*w[i]+b]-s*v[i]) f[i][C]=max(f[i−1][s∗w[i]+b]−s∗v[i]),求出最大值后加上av[i]即可。
当使用该式进行递推时,随着s的增大,我们将焦点聚集在放在 s = s 0 s=s_0 s=s0时的情况。此时我们从被扣除 0 , 1 , 2 , . . . , s 0 0,1,2,...,s_0 0,1,2,...,s0件i物品的价值中,找到最大的价值。如果此时我们给每一个式子都补充 s 0 s_0 s0件i物品,式子的含义就变为,背包内放入 s 0 , s 0 − 1 , s 0 − 2 , . . . , 0 s_0,s_0-1,s_0-2,...,0 s0,s0−1,s0−2,...,0件i物品的最大价值,而此时这个式子恰好对应背包容量为 s 0 ∗ w [ i ] + b s_0*w[i]+b s0∗w[i]+b时的最优解,这样,当s递增到a时,我们就求得了背包容量为 a ∗ w [ i ] + b a*w[i]+b a∗w[i]+b的最优解,根据a和b的定义,a=v/w[i],b=v%w[i],所以 a ∗ w [ i ] + b = v a*w[i]+b=v a∗w[i]+b=v。
于是通过s的递增,我们求得了 f [ i ] [ b , b + w [ i ] , b + 2 w [ i ] , b + 3 w [ i ] , . . . , C ] f[i][b,b+w[i],b+2w[i],b+3w[i],...,C] f[i][b,b+w[i],b+2w[i],b+3w[i],...,C]的值。然而回到本节第二自然段的问题:如何求得 f [ i ] [ 0 , 1 , 2 , . . . C ] f[i][0,1,2,...C] f[i][0,1,2,...C]中剩余未求的值?
如果我们改变b的大小,让b从0递增到w[i]-1,在每一个b固定的时候,求出所有可能的背包大小的最优解,即可将[0,1,2,3,…C]填满。这是有一个疑问:b=C%w[i],那么当C固定的时候,b不就固定了吗,怎么可以让它从0递增到w[i]-1呢?解决这个疑问的方法就是:无论对于容量多大的背包,其在装满i物品之后的剩余空间,只会是0到w[i]-1。如果我说当前你要求的是f[i][j],你是不是要让b=j%w[i]并且a=j/w[i]?当j在变化的时候,b与a也是在变的对吧,但是b无论怎么变,都是在0到w[i]-1这个范围内的。而a的取值,因为 j < = C , 即 a ∗ w [ i ] + b < = C j<=C,即a*w[i]+b<=C j<=C,即a∗w[i]+b<=C,所以 a < = ( C − b ) / w [ i ] a<=(C-b)/w[i] a<=(C−b)/w[i],那么我让b从0到w[i]-1递增,是不是就可以等价于覆盖全部j的取值?这样一来f[i][0,1,2,3,4,…,C]就都可以求出来了,从不可持续的递推变成了可持续的递推,有了这个我们才可以求f[i+1][C]的值。
在上面的分析中,我们一直漏掉了一个条件,就是i物品最多放入lim件。当我们事先从背包中扣除s件i物品的价值后,随后又补充了a件i物品的价值,等价于放入了a-s件i物品的价值。在
f [ i − 1 ] [ 0 + b ] , f [ i − 1 ] [ 1 ∗ w [ i ] + b ] − v [ i ] , f [ i − 1 ] [ 2 ∗ w [ i ] + b ] − 2 ∗ v [ i ] , . . . f [ i − 1 ] [ s ∗ w [ i ] + b ] − s ∗ v [ i ] f[i-1][0+b],f[i-1][1*w[i]+b]-v[i],f[i-1][2*w[i]+b]-2*v[i],...f[i-1][s*w[i]+b]-s*v[i] f[i−1][0+b],f[i−1][1∗w[i]+b]−v[i],f[i−1][2∗w[i]+b]−2∗v[i],...f[i−1][s∗w[i]+b]−s∗v[i]中,
我们为每一项都补充a件i物品,就变成了:
f [ i − 1 ] [ 0 + b ] + s ∗ v [ i ] , f [ i − 1 ] [ 1 ∗ w [ i ] + b ] + ( s − 1 ) ∗ v [ i ] , f [ i − 1 ] [ 2 ∗ w [ i ] + b + ( s − 2 ) ∗ v [ i ] , . . . f [ i − 1 ] [ s ∗ w [ i ] + b ] f[i-1][0+b]+s*v[i],f[i-1][1*w[i]+b]+(s-1)*v[i],f[i-1][2*w[i]+b+(s-2)*v[i],...f[i-1][s*w[i]+b] f[i−1][0+b]+s∗v[i],f[i−1][1∗w[i]+b]+(s−1)∗v[i],f[i−1][2∗w[i]+b+(s−2)∗v[i],...f[i−1][s∗w[i]+b]
我们可以看出对于第一项含义是放入s件i物品,第二项含义是放入s-1件i物品,最后一项含义是放入0件i物品。那么此时的这些项中,最可能不符合实际情况的就是第一项:放入s件i物品。如果i物品的个数lim小于s,那么我们就不可能放入s件物品,最多放入lim件。那么界线怎么找?我们知道最后1项代表放0件i物品,倒数第2项代表放1件i物品,那么倒数第lim+1项即代表放入lim件i物品,也就是说,再往前数就是对于当前容量为 s ∗ w [ i ] + b s*w[i]+b s∗w[i]+b的背包来说不可能的情况了。倒数第lim+1项为正着数的第s-lim项。这里就是单调队列发挥用处的时候了。当前这个求最大值的问题可以转化成为,有s+1个数的数组,区间大小为lim+1,那么该区间内的最大值为多少?使用单调队列可以直接获得。如果当前单调队列的队首位置小于s-lim,那么就要将队首出队,将队首位置向后移才可以。
至此问题分析结束。
最外层循环依旧是物品种类的递增,之后是余数的递增,最内层循环是 f [ i ] [ b + s ∗ w [ i ] ] f[i][b+s*w[i]] f[i][b+s∗w[i]]的计算和单调队列的维护。这里只给出伪代码。
for i=1 to n
lim=min{V/w[i],q[i]};
for b=0 to w[i]-1
重置单调队列
for s=0 to (V-b)/w[i]
value=f[i-1][s*w[i]+b]-s*v[i];
pos=s;
新值加入单调队列
维护单调队列区间
f[i][s*w[i]]=单调队列最大值+s*v[i];
end
end
end