动态规划之背包问题

前言

背包问题是动态规划的一系列经典问题,想必不少人刚接触动态规划问题时都是从这一类问题入手。最近刷LintCode时遇到了动态规划的专题,结合一直想读却一直懒得读的《背包问题九讲》,特意开一篇Blog学习一下。

正题

1. 01 背包问题

有N件物品和一个容量为V的背包。放入第i件物品耗费的费用是Ci,得到的价值是Wi。求解将哪些物品装入背包可以使价值总和最大。

基本思路

这是最基本的背包问题,特点是:每种物品仅有一件,可以选择放或者是不放。
用子问题定义状态:即F[i, v]表示前i件物品放入一个背包容量为v的背包时可以获得的最大价值。则其状态转移方程是:

F[i, v] = max{F[i-1, v], F[i-1, v-Ci]+Wi}

这个方程非常重要,基本上所有的背包相关的问题的方程都是由它衍生出来的。具体的思路就是针对第i件物品,有两种策略,放还是不放,所获得的最大值就是两种选择中的最大值。
伪代码如下:

F[0, 0...V] ← 0
for i ← 1 to N
    for j ← 0 to Ci-1
        F[i,v] ← F[i-1, v]
    for j ← Ci to V
        F[i,v] ← max{F[i-1, v], F[i-1, v-Ci]+Wi }
优化空间复杂度

以上方法的空间和时间复杂度均为O(VN),其中时间复杂度已经不能在优化了,但是空间复杂度还可以优化到O(V)。
先考虑上面讲的思路如何实现,肯定是有一个主循环i←1…N,每次算出来二维数组F[i, 0…V]的所有值。那么,如果只用一个数组F[0…V],能不能保证第i次循环结束以后F[v]中表示的就是我们定义的状态F[i,v]呢?F[i,v]是由F[i-1, v], F[i-1, v-Ci]两个子问题推导而来的,能否保证在推F[i, v]时能够取用F[i-1, v],F[i-1, v-Ci]的值呢?也就是说,如果用一维数组F[0..V]的话,我就需要在第i次循环时F[v]的更新要早于F[v-Ci],这样的话F[v-Ci]和F[v]在取用时都还没有被第i次循环更新,所以代表的就是第i-1次的值。
伪代码如下:

F[0...V] ← 0
    for i ← 1 to N
        for v ← V to Ci
            F[v] ← max{F[v], F[v-Ci]+Wi}
初始化的细节问题

我们看到的求最优解的背包问题当中,事实上有两种不太相同的问法。有的题目要求恰好装满背包时的最优解,有的题目则并没有要求必须把背包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同。
如果是第一种问法,要求恰好装满背包,那么在初始化时除了F[0]为0,其他的F[1…V]均设为负无穷,这样就可以保证最终得到的F[V]是一种恰好装满背包的最优解。
如果没有这种要求,那么只需要将F[0…V]全部初始化为0即可。
可以这样理解:初始化的F数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可以在什么也不装入且价值为0的情况下被“恰好装满”,其他容量的背包均没有合法解,属于未定义的状态,因此都要被初始化为负无穷(INT_MIN)。

LintCode 相对应的题目

02 完全背包问题

有N种物品和一个容量为V的背包,每种物品都有无限件可以使用。放入第i种物品的费用是Ci,价值是Wi,求解:将哪些物品装入背包,可以使这些物品的耗费费用总和不超过背包容量,且价值总和最大。

基本思路

这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略并非取或不取两种,而是有取0件,取1件,取2件…直至取V/Ci件等许多种。
如果仍按解01背包问题时的思路,令F[i, v]表示前i种物品放入一个容量为v的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程:

F[i, v] = max{F[i-1, v-kCi] + kWi, 0 <= kCi <= v }

但是这种方法的复杂度还是比较高的,代码实际上需要一个三重循环来完成。

一个简单有效的优化

若两件物品i,j满足Ci<=Cj且Wi>=Wj,则可以将物品j直接去掉,不做任何考虑。这个优化的正确性是显然的。

转化为01背包问题求解

01背包问题是最基本的背包问题,我们可以考虑把完全背包问题转化为01背包问题来解。
最简单的想法是,考虑到第i件物品最多选V/Ci件,于是可以把第i种物品转为为V/Ci件费用及价值均不变的物品,然后求解这个01背包问题。
更高效的转化方法是:把第i种物品拆成费用Ci2^k, 价值为Wi2^k的若干件物品,其中k取遍满足Ci2^k <= V的所有非负整数。这是一种二进制的思想。(emmm…..)

O(VN)的算法

这个算法使用一维数组,先看伪代码

F[0...V] ← 0
    for i ← 1 to N
        for v ← Ci to V
            F[v] ← max{F[v], F[v-Ci]+Wi}

你会发现,这个伪代码与01背包问题的伪代码只有v的循环次序不同而已。
为什么这个算法可行呢?首先想想为什么01背包中要按照v递减的次序来循环。让v递减是为了保证第i次循环中的状态F[i, v]是由F[i-1, v-Ci]递推而来。换句话说,这是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这个策略时,依据的是一个绝无已经选入第i件物品的子结果F[i-1, v-Ci]。而现在完全背包的特点恰是每种物品可以选无限件,所以在考虑”加选一件第i种物品”这种策略时,却正需要一个可能已经选入第i种物品的子结果F[i, v-Ci],所以就可以并且必须采用v递增的顺序循环。
完全背包的递归公式:

F[i, v] = max{F[i-1, v], F[i, v-Ci]+Wi }

其中F[i, v-Ci]表示在前i件物品中容量为v-Ci时,尽可能的放入第i件物品时所能得到的最大价值。F[i, v]就可以再次基础上再放入一个物品i。

总结

事实上,对每一道动态规划的题目都思考其方程的意义以及如何得来,是加深对动态规划的理解,提高动态规划功力的好方法。
最后贴一个LintCode BackPack六讲,虽然我只在LintCode上发现了3道题..
https://segmentfault.com/a/1190000006325321

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值