[经典] 背包问题(一)
【1】01背包
N个物品,占容c[i],价值w[i],放入1个容量为V的背包,使得总价值最大
分析:每种物品仅有一件,可以选择放或不放
转移方程:opt[i][v] = max{opt[i - 1][v], opt[i - 1][v - c[i]] + w[i]}
复杂度:时间空间均为O(NV),空间复杂度可压缩,用opt[v]表示,但需要注意的是v必须从V到0遍历,否则逻辑错误;
初始化技巧:如果要求刚好装满,则设为负无穷;如果只要求最大,则设为0即可。
事实上,由于对某件物品的01处理问题会在不同情景下都被调用,所以可以写成一个调用函数ZeroOnePack(cost, weigth),其中v的下限可被优化
procedure ZeroOnePack(cost,weight) for v=V..cost f[v]=max{f[v],f[v-cost]+weight}
所以本题的伪代码可以写成
for i=1..N ZeroOnePack(c[i],w[i]);
另外,其实下限可以进一步被优化,当V很大时有效,此时本题的伪代码可以写成
for i=1..n bound=max{V-sum{w[i..n]},c[i]} for v=V..bound
f[v]=max{f[v],f[v-cost]+weight}
【2】完全背包
特点是每件物品的数目都是没有限制的,可以由01背包延展出解法,即f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]}, 0<=k*c[i]<=v;复杂度很显然上升了O(v/c[i]),所以尝试找到优化的方法。最简单的优化:c[i] < c[j] && w[i] > w[j],则显然应该选择w[i],去除j物品。
先是最小均分,将每个物品当成有v/c[i]个,则完全背包等效于01背包,复杂度仍比01上升O(v/c[i]);然后是指数拆分,将每个物品拆分成费用为c[i]*2^k、价值为w[i]*2^k的子物品,则复杂度只上升O(log(v/c[i]))。
经典解法:复杂度与01背包一样,只有O(VN)。动态规划方程为f[i][v]=max{f[i-1][v],f[i][v-c[i]]+w[i]},而空间复杂度为O(V)的算法只需要将v从0到V顺序遍历。
procedure CompletePack(cost,weight) for v=cost..V f[v]=max{f[v],f[v-c[i]]+w[i]}
事实上,v循环与i循环的次序可以颠倒
【3】多重背包
特点是每件物品的数目有中庸的限制,即第i件物品的数目限制是n[i]。最直接的方法也是拆分01背包,f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]}, 0<=k<=n[i],复杂度为O(V*Σn[i]);然后是指数拆分,也是经典解法,复杂度为O(V*Σlog n[i])。
procedure MultiplePack(cost,weight,amount) if cost*amount>=V CompletePack(cost,weight) return integer k=1 while k<amount ZeroOnePack(k*cost,k*weight) amount=amount-k k=k*2 ZeroOnePack(amount*cost,amount*weight)
还有复杂度更低的方法,复杂度与01背包一样,只有O(VN),方法是单调队列优化,超了NOIP范围。
【4】混合背包
将前三种背包过程分类混合使用,伪代码:
for i=1..N if 第i件物品属于01背包 ZeroOnePack(c[i],w[i]) else if 第i件物品属于完全 CompletePack(c[i],w[i]) else if 第i件物品属于多重背包 MultiplePack(c[i],w[i],n[i])
【5】二维费用背包
多用一个状态记录多出来的费用,设f[i][v][u]表示前i件物品付出两种代价分别为v和u时可获得的最大价值。状态转移方程就是:
f[i][v][u]=max{f[i-1][v][u],f[i-1][v-a[i]][u-b[i]]+w[i]}