《背包问题九讲》学习笔记

《背包问题九讲》学习笔记

第一讲 01背包问题

有一些值得学习的手法,在此做一个整理:

状态转移方程 : f[i][v]=max{f[i - 1][v], f[i - 1][v - c[i]] + w[i]}

事实上,在实现该dp时,会有1-n的外层循环,我们考虑用一个一维数组f[n],在第i次循环的时候可以将新f[n]更新为原f[i][n]的状态,从而节省了记录中间量的空间消耗。因为v-c[i]总是要比v小的,所以我们只需要倒序遍历就可以完成更新工作。
在倒序时,还有一个地方可以进行优化:对于消耗为cost的物品,只需要从V遍历到cost就足够了,因为比cost小的部分不会受到该物品的影响;另一方面,最后只要f[n]的值,所以根据递推式,我们只需要知道f[v - w[n]]的值即可,以此类推,对于第j个背包只需要知道f[v - sum{w[j…n]}的值即可;综上我们可以得到一个更快的遍历方式:

for i = 1..n 
  	 bound = max{V - sum{w[i...n]}, c[i]}
     for v = V...bound

此外,在进行初始化时:如果要求的是恰好装满背包,除了f[0] = 0,其余都初始化为-INIFITY;不然,全部初始化为0即可。

附上过程代码(未进行上述提到的bound优化):

procedure ZeroOnePack(cost,weight)
    for v=V..cost
        f[v]=max{f[v],f[v-cost]+weight}
第二讲 完全背包问题
递推式:f[i][v] = max{f[i - 1][v - k * c[i]] + k * w[i] | 0 <= k * c[i] <= v}

根据第一讲学习到的递推手法,很自然的可以得到这个递推式。但是它的复杂度往往不能忍受,所以考虑一些优化办法:

  1. 显然的,对于同等cost的物品,我们会优先取价值最高的那一部分。我们可以通过这个思想,用计数排序的办法获得每一个消耗下可以获得的最大收益。当然了,这个办法对于那些一种物品都去不掉的情况的优化性能就不尽人意了,所以这只是一种朴素的贪心优化办法。
  2. 对于消耗为cost的某个物品,显然它最多只可以取V/cost件,所以可以将无限次取用化为有限次取用,进一步即可将完全背包化为01背包问题。这个办法其实并不比暴力求解好很多,但是体现了一种化归的思想值得我们借鉴。此外,有一种更加高效的拆分办法,相比于拆分为V / cost件物品,我们可以通过二进制的思想,将二的幂次件看作一个整体,这样就可以进一步进行优化算法了。
  3. 之后是更加优化的办法,复杂度只有O(VN):
for i=1..N
	for v = cost..V
	f[v] = max{f[v], f[v - cost] + weight}

比起第一讲,这里使用了顺序遍历,是有一定道理的:对于01背包,只有取和不取两种情况,如果顺序遍历的话会导致此时的f[v]包括了取当前物品的情况,与01背包所要求的不符。但是在完全背包中对物品选取次数不做要求,因而就需要顺序遍历,来保证目前的f[v]是在各种情况下都是最大的价值。

附上过程代码:

procedure CompletePack(cost,weight)
    for v=cost..V
        f[v]=max{f[v],f[v-c[i]]+w[i]}
第三讲 多重背包问题

与完全背包不同的是,这里每一件物品不能够任意取了,各自有一个规定的上限n[i],但是根据原理,类似的我们有递推式:

f[i][v] = max{f[i - 1][v - k * c[i]] + k * w[i] | 0 <= k <= min{n[i], v / c[i]}

对于这个问题,我们先考虑一下二进制的写法。

使这些系数分别为1,2,4,…,2(k-1),n[i]-2k+1,且k是满足n[i]-2k+1>0的最大整数。例如,如果n[i]为13,就将这种物品分成系数分别为1,2,4,6的四件物品。

贴一段《九讲》中的代码:

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)

当cost * amount >= V的时候,amount相当于可以在V的范围内任意取,那么自然也就符合完全背包的条件了;之后再通过循环的手法,实现二进制思想,并且获得最大的那个k和对应的amount,跳出循环后对amount再实现一次01背包过程。

至于O(VN)做法,需要用到单调队列的思想。原文是这么说的:

多重背包问题同样有O(VN)的算法。这个算法基于基本算法的状态转移方程,但应用单调队列的方法使每个状态的值可以以均摊O(1)的时间求解。由于用单调队列优化的DP已超出了NOIP的范围,故本文不再展开讲解。我最初了解到这个方法是在楼天成的“男人八题”幻灯片上。

洛谷日报_单调队列
有关的题目日后有待挖掘。

基于三种基本背包问题的拓展

混合背包问题: 对于每一个物品的要求,采取不同的策略即可。

二维费用的背包问题:

f[i][v][u] = max{f[i - 1][v][u], f[i - 1][v - a[i]][u - b[i]] + w[i]}

类似一维背包的讨论,面对01时逆序遍历,面对完全背包时顺序遍历,面对多重背包拆分考虑,从而也可将其化为二维数组进行存储。

如果题面在价值上做了要求后,也对物品总数做了要求,这实际上也是一个隐“二维背包”问题。在实际处理问题中,需要仔细阅读题面,选取合适的背包。

从另一个角度考虑,二维费用的背包可以这样子看:

另一种看待二维背包问题的思路是:将它看待成复数域上的背包问题。也就是说,背包的容量以及每件物品的费用都是一个复数。而常见的一维背包问题则是实数域上的背包问题。(注意:上面的话其实不严谨,因为事实上我们处理的都只是整数而已。)所以说,一维背包的种种思想方法,往往可以应用于二位背包问题的求解中,因为只是数域扩大了而已。

第六讲 分组的背包问题

f[k][v]表示前k组消耗v空间所能获得最大价值,那么就有:

递推表达式:f[k][v] = max{f[k - 1][v], f[k - 1][v - c[i]] + w[i] | 物品i属于组k}

循环代码如下:

for 所有的组k
    for v=V..0
        for 所有的i属于组k
            f[v]=max{f[v],f[v-c[i]]+w[i]}

特别需要注意三重循环的顺序!

对于其顺序、原理,我们可以作这样子粗浅的理解:如果先遍历 i,再对容量v进行遍历,事实上进行的是对组k的01背包操作,那么显然,k中的物品可以被选取不止1件,这就不符合组k中至多只能取一件的要求了;再说明先对v遍历后对i遍历的合理性,对于某个给定的V0,循环操作只会在组k中的所有物品中寻找一个 i(因为不断地取最大值,那么最后一定只会取到某“一个”物品,使得在当前空间消耗V0 下的价值最大),那么这自然是满足分组背包的约束了。

第七讲 有依赖的背包问题

洛谷oj_金明买家具
这是一道相当简化的依赖背包问题,但是能够比较有效地反映出如何处理依赖背包问题的一个思路。在主件及其一堆附件中,我们有可以做很多决策,以这些决策作为新背包的元素,那么就可以将依赖背包化归至多重背包的问题。

若是考虑更一般的问题,即物品间的依赖关系通过森林的形式给出(此时唯一的约束就是每个物品最多依赖一个物品且不出现循环依赖的情况)。为了处理仍然是另外一些物品的主件的附件,需要将该物品的决策先转换为物品集,再往上处理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值