本文背包问题选自 《算法图解》一书
Overview
问题示例(背包问题)
简单解法
动态规划
注:中间图示略。以下跳到最后一行:
Python 代码实现(动态规划)
注意,这里的一步一步实现过程很详细。
但是目前没有用语言解释代码,请悉知。
# weigth, worth
product = [{"guitar": (1, 1.5)},
{"box": (4, 3)},
{"laptop": (3, 2)}]
# 1kg, 2kg, 3kg, 4kg use kg instand of pound
CELL = [[None, None, None, None], # guitar
[None, None, None, None], # box
[None, None, None, None],] # laptop
CELL[0][0] = product[0]
CELL
判断改行商品的重量是否能够放入:
CELL[0][1] = product[0] if (1 + 1) >= list(product[0].values())[0][0] else None
CELL[0][2] = product[0]; CELL[0][3] = product[0]
因为知道第一行没有问题,所以暂时用结论写出这个代码。
后面在一步步手动填充好 CELL 数组之后,熟悉了算法的实现之后,
会返回来更新真实的实现。
第二行
CELL[1][0] = product[1] if (0 + 1) >= list(product[1].values())[0][0] and list(product[1].values())[0][1] >= list(CELL[0][0].values())[0][1] else CELL[0][0]
是否使用该行对应的 product 需要判断当前行的 product 是否重量放得下小背包(列),以及价值是否比上一行的本列更高
第二行第二列
CELL[1][1] = product[1] if (1 + 1) >= list(product[1].values())[0][0] and list(product[1].values())[0][1] >= list(CELL[0][1].values())[0][1] else CELL[0][1]
第二行第三列
CELL[1][2] = product[1] if (2 + 1) >= list(product[1].values())[0][0] and list(product[1].values())[0][1] >= list(CELL[0][2].values())[0][1] else CELL[0][2]
第二行第四列
CELL[1][3] = product[1] if (3 + 1) >= list(product[1].values())[0][0] and list(product[1].values())[0][1] >= list(CELL[0][3].values())[0][1] else CELL[0][3]
? boom ? 这一列的背包放入了当前行的商品
第三行
第一列
# column: 0; row: 2(0, 1, 2)
if (0 + 1) >= list(product[2].values())[0][0]: # weight OK
if list(product[2].values())[0][1] >= list(CELL[1][0].values())[0][1]:
CELL[2][0] = product[2]
if not CELL[2][0]:
CELL[2][0] = CELL[1][0]
第三行第二列
# column: 1; row: 2(0, 1, 2)
if (1 + 1) >= list(product[2].values())[0][0]: # weight OK
if list(product[2].values())[0][1] >= list(CELL[1][1].values())[0][1]:
CELL[2][1] = product[2]
if not CELL[2][1]:
CELL[2][1] = CELL[1][1]
第三行第三列
# column: 2; row: 2(0, 1, 2)
if (2 + 1) >= list(product[2].values())[0][0]: # weight OK
if list(product[2].values())[0][1] >= list(CELL[1][2].values())[0][1]:
CELL[2][2] = product[2]
if not CELL[2][2]:
CELL[2][2] = CELL[1][2]
可以看到,第三行的商品是笔记本电脑,而它在第三列(3kg)被放入背包
第三行第四列
# column: 3; row: 2(0, 1, 2)
if (3 + 1) >= list(product[2].values())[0][0]: # weight OK
if list(product[2].values())[0][1] >= list(CELL[1][3].values())[0][1]:
CELL[2][3] = product[2]
if not CELL[2][3]:
CELL[2][3] = CELL[1][3]
这里会放入音响,因为它可以放入第四列 4kg的背包中,而且单品价值最高。
结果不是语言的,因为上面的代码中还没有考虑放入该行对应的商品(笔记本电脑)之后剩余的重量可以放入的最大价值,再和本列的上一行比较。
加入剩余重量可以放入的商品价值再决定 - 正确的算法
CELL[2][3] = None
# column: 3; row: 2(0, 1, 2)
if (3 + 1) >= list(product[2].values())[0][0]: # weight OK
product_worth = list(product[2].values())[0][1]
left_wigth = ((3+1) - list(product[2].values())[0][0])
left_wigth_worth = list(CELL[2][left_wigth - 1].values())[0][1]
if (product_worth + left_wigth_worth)>= list(CELL[1][3].values())[0][1]:
CELL[2][3] = [product[2], CELL[2][left_wigth -1]]
if not CELL[2][3]:
CELL[2][3] = CELL[1][3]
最后的整理的算法实现:
将数组清空
# 1kg, 2kg, 3kg, 4kg use kg instand of pound
CELL = [[None, None, None, None], # guitar
[None, None, None, None], # box
[None, None, None, None],] # laptop
先初始化第一行
for i in range(4):
CELL[0][i] = product[0] if (i + 1) >= list(product[0].values())[0][0] else None
动态规划算法
for r in range(1, 3):
for c in range(4):
if (c + 1) >= list(product[r].values())[0][0]: # weight OK
product_worth = list(product[r].values())[0][1]
left_wigth = ((c + 1) - list(product[r].values())[0][0])
if left_wigth > 0:
left_wigth_worth = list(CELL[r][left_wigth - 1].values())[0][1]
if (product_worth + left_wigth_worth)>= list(CELL[r - 1][c].values())[0][1]:
CELL[r][c] = [product[r], CELL[r][left_wigth -1]]
else:
if product_worth >= list(CELL[r - 1][c].values())[0][1]:
CELL[r][c] = product[r]
if not CELL[r][c]:
CELL[r][c] = CELL[r - 1][c]
掌声响起来 ? ? ? ? ?
最后一点点提醒,算法中有几处需要改进的:
- 因为这里的小背包是整数,而且每个精细度都计算过,所以这里
left_wigth_worth = list(CELL[r][left_wigth - 1].values())[0][1]
不会发生IndexError
, 但是未必每个动态规划都可以这样,所以这是一个代码中省略过去的问题。- 可以看到上面放入组合商品和只有一个商品的“类型”不一样,组合商品使用的列表。实际上应该全部都是列表,这一点只需要在代码中做细微的调整就可以 (同时这种调整在那种很有可能会超过 2 个组合的情况下很重要)。
- 上面的结果显得有点儿依赖顺序,实际上和商品放在哪一行用来放入背包无关。上面的顺序设计是为了从简单的情况一步步到复杂的情况(对于要放入背包的商品组合有更多的判断条件),便于理解和分析。在掌握之后,和最终的实现上,它是和商品在哪一行的顺序无关的。
- 上面的代码没有考虑一个商品在表格中的某一格使用多次的情况,实际上这是不可能发生的,因为比如当你考虑 2kg 的背包的时候,你可以放入两个 1kg 的吉他,但是吉他只有一把。所以如果调整了上面已经为了方便理解而设计过的顺序,会出现这这里说的情况,这是在实际应用中的代码中需要完善的。
- 调整了商品在哪一行的顺序之后,会出现一些错误,这是因为可能重量超过了小背包的重量,放不下值为
None
,而后面的格子又依赖前面的格子取值来判断,就会发生AttributeError
这样的情况,所以改进代码的时候也要考虑这一点。- 可能还有其它我在实现的过程中一时没有注意到的问题…
不过为了实现方便,前面代码中没有完善上面提到的问题。这里就是做一下提醒。