上一节中对动态规划有了一个初步的认识,现在通过更多的例子来加深印象,在网上搜索了一下关于动态规划的经典问题,其中第一个就是所谓“背包问题”,事实上它也很类似上一节中的钢条切割,稍有不同。
背包问题
给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价值最高。
不同于上节钢条切割的地方是,物品的重量不是连续的,所以事先并不能知道任意物品组成的总重会的集合。
为了便于编程实现,将问题具体化,比如使用上一节中的数据,重量价格表P:
重量: 2 3 4 5 6 7 8 9 10
价值: 5 8 9 10 17 17 20 24 30
但是上一节中我们的输入不能>10,在背包问题中,没有这种限制。
分析
以r(n)
表示最大重量为n时的最优解,p(i)
为重为i
的物品的价值,对问题进行分解:
r(n) = max[ p(i) + r(n-i) ] #其中 i(min)<=i<=i(max)
如此问题的分解与结合就完成了, 需要注意的细节有
- n 或 n-i 不一定能通过组合得到
- 确保r(n)递增
自底向上
尝试使用bottom up的方式,从小到大求解最优解,直到满足问题的输入值n。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| def bottom_up_bag(n, p): # make sure that records in mem is an optimal solution # init. { n: [ maxWeight, "combination"] } mem = {0: [0, ""]} keys = p.keys() keys.sort() max = 0 for sum in range(min(p.keys()), n + 1): for i in keys: if i > sum: break sub = sum - i if sub in mem: t = p[i] + mem[sub][0] if t > max and (sum not in mem or t > mem[sum][0]): mem[sum] = [t, mem[sub][1] + str(i) + " "] max = t return mem
|
测试和输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| # 测试代码 # 先用较少的物品,作便于分析 p = {3: 8, 5: 10} r = bottom_up_bag(14,p) for i in r: print i, r[i] ######以下为输出###### 0 [0, ''] 3 [8, '3 '] 5 [10, '5 '] 6 [16, '3 3 '] 8 [18, '5 3 '] 9 [24, '3 3 3 '] 11 [26, '5 3 3 '] 12 [32, '3 3 3 3 '] 14 [34, '5 3 3 3 ']
|
可以看到输出并不是连续的,4、7等是因为无法组合得到,而10是因为5+5组合价值为20,不及重量为9最优解24
这些不连续的值的最优解为与其相邻较小值的最优解,如背包容量为7时,最优解同容量6,为3+3;容量为10时,同容量9,取3+3+3
分析
算法的正确性是如何保证的?依然如前一节中所说,自底向上的算法的关键在于计算子问题时,确保更小的子问题已经求解。
初始条件为0:0,即重为0时价值为0;
最小子问题规模为最小单个物品重量,依次递增;
尝试所有可能的拆分:单个物品+子最优解(规模更小,一定已知),并取最大值(且要求较前一容量递增)
这种方法不是很严谨,因为无法预知可能组合成哪些数值,所以采用了++1这样的方式来试探,如果物品重量值很大,性能将非常低下
带备忘的自顶向下
这是更适合背包问题的解法,同样的分解思路,不过这次是自顶向下的递归调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| def recurse_bag(n, p): # make sure that records in mem is an optimal solution mem = {0: [0, ""]} keys = p.keys() keys.sort() def inner(n, p): if n in mem: return mem[n] if n < min(keys): return [0,""] for i in keys: if i <= n: s = inner(n - i, p) t = p[i] + s[0] if n not in mem or t > mem[n][0]: mem[n] = [t, "%s %s" % (s[1], str(i))] else: break return mem[n] inner(n,p) return mem
|
子问题一旦求解一次,就存储在备忘map中(变量mem),每次求解向查找备忘,无记录才求解,递归的另一个好处就是不必在意物品能组合成哪些数值。
对比测试与输出
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| p = {3: 8, 5: 10, 8: 20} vol = 14 r1 = recurse_bag(vol, p) r2 = bottom_up_bag(vol, p) k1 = r1.keys() k2 = r2.keys() k1.sort() k2.sort() for i in range(max(len(k1), len(k2))): if i < len(k1): print k1[i], r1[k1[i]], "\t\t\t\t\t\t", if i < len(k2): print k2[i], r2[k2[i]], print ""
|
输出情况
1 2 3 4 5 6 7 8 9
| 0 [0, ''] 0 [0, ''] 3 [8, ' 3'] 3 [8, ' 3'] 4 [8, ' 3'] 5 [10, ' 5'] 5 [10, ' 5'] 6 [16, ' 3 3'] 6 [16, ' 3 3'] 8 [20, ' 8'] 8 [20, ' 8'] 9 [24, ' 3 3 3'] 9 [24, ' 3 3 3'] 11 [28, ' 8 3'] 11 [28, ' 8 3'] 12 [32, ' 3 3 3 3'] 14 [36, ' 8 3 3'] 14 [36, ' 8 3 3']
|
二者的记录的值并不一样,但对应的容量和最优解是一致的,递归比较能体现求解的过程,bottom up则列出了规模范围内所有的解。