动态规划是一种非常重要的方法,但是也很难理解。在此将通过实例来浅显的探讨一下动态规划的思想以及在早期的生物信息学中如何应用动态规划进行全局比对。
1、Dynamic programming —— 从背包问题说起
关于理解动态规划的例子网上有很多,此处以《图解算法》中的一个背包问题来入门动态规划。
1.1 背包问题:假设你是个小偷,背着一个可装4磅东西的背包。你可盗窃的商品有如下三件,为了使得盗窃商品的价值最大,该如何选择呢?
很多人可能第一反应便是枚举法:将所有可能的装法都列出来,比较大小,取最大者即可。由于这里的商品较少,很容易就能列出所有的8种携带方法,然后去掉超重的,在剩下的组合中选择价值最大的即可。但是这种算法的复杂度为O(2**n),非常的慢。
稍微学过一些算法的人可能会想到贪心算法:先拿值钱的,能拿多少拿多少,但是在这里贪心算法得到的真的是最优解吗?贪心算法偷走的商品为音响(3000磅),但是这里最优解明显是(电脑+吉他,3500磅)。这也暴露了贪心算法的一个重大缺点,贪心算法得到的常常只是接近全局最优解而非真正的全局最优解,当然这不在我们的讨论范畴内。
下面来看看如何使用动态规划来得到最优解。
1.2 理解动态规划:从填网格开始
动态规划的中心思想是将大问题(网格)分解为小问题(小网格),先解决小问题的最优解最后综合得到大问题的最优解。上面背包问题的网格如下:
1.2.1 吉他行
这是吉他行,意味着你将尝试将吉他装入背包。在每个单元格,都需要做一个简单的决定:偷不偷吉他?别忘了,你要找出一个价值最高的商品集合。
第一个单元格表示背包的容量为1磅。吉他的重量也是1磅,这意味着它能装入背包!因此这个单元格包含吉他,价值为1500美元。
与这个单元格一样,每个单元格都将包含当前可装入背包的所有商品。
这行的其他单元格也一样。别忘了,这是第一行,只有吉他可供你选择。换言之,你假装现在还没法盗窃其他两件商品。
别忘了, 你要做的是让背包中商品的价值最大。 这行表示的是当前的最大价值。它指出,如
果你有一个容量4磅的背包,可在其中装入的商品的最大价值为1500美元(现在只有一把吉他给你偷)。
1.2.2 音响以及其他行
我们来填充下一行——音响行。你现在出于第二行,可偷的商品有吉他和音响。在每一行,可偷的商品都为当前行的商品以及之前各行的商品。因此,当前你还不能偷笔记本电脑,而只能偷音响和吉他。我们先来看第一个单元格,它表示容量为1磅的背包。在此之前,可装入1磅背包的商品的最大价值为1500美元。
该不该偷音响呢?
接下来的两个单元格的情况与此相同。在这些单元格中,背包的容量分别为2磅和3磅,而以前的最大价值为1500美元。
由于这些背包装不下音响,因此最大价值保持不变。
此时你更新了最大价值!如果背包的容量为4磅,就能装入价值至少3000美元的商品。在这个网格中,你逐步地更新最大价值。
1.2.3 电脑行
电脑行的填值情形与音响行相似,此处为就不一步步推理了。
对于容量为4磅的背包,填最后一个网格的情况很有趣。这是非常重要的部分。当前的最大价值为3000美元,你可不偷音响,而偷笔记本电脑,但它只值2000美元。价值没有原来高。但等一等,笔记本电脑的重量只有3磅,背包还有1磅的容量没用!
你可能始终心存疑惑:为何计算小背包可装入的商品的最大价值呢?但愿你现在明白了其中的原因!余下了空间时,你可根据这些子问题的答案来确定余下的空间可装入哪些商品。笔记本电脑和吉他的总价值为3500美元,因此偷它们是更好的选择。
最终的网格类似于下面这样。
答案如下:将吉他和笔记本电脑装入背包时价值最高,为3500美元。
其实,计算每个单元格的价值时,使用的公式都相同。
这个公式如下:
2、最长公共子串
在进行探讨最长公共子串问题前,先来总结下前面背包问题可得到的一些结论:
- 动态规划可帮助你在给定约束条件下找到最优解。在背包问题中,你必须在背包容量给定的情况下,偷到价值最高的商品。
- 在问题可分解为彼此独立且离散的子问题时,就可使用动态规划来解决。
每种动态规划解决方案都涉及网格。 - 单元格中的值通常就是你要优化的值。在前面的背包问题中,单元格的值为商品的价值。
- 每个单元格都是一个子问题,因此你应考虑如何将问题分成子问题,这有助于你找出网格的坐标轴。
2.1 最长子串问题:某天一位同学想在你自建的词典上面查询单词fish,但是一不小心输入了hish。但是词典中没有hish,在你的字典中,根本就没有这样的单词,但有几个类似的单词fish or vista?
hish和fish都包含的最长子串是什么呢? hish和vista呢?这就是我们要计算的值。
2.2 首先,我们要做的依然是画网格
在这里我们就不一步步画网格推导了,直接给出最终的网格:
在上面的背包问题中,我们给出了一个公式来计算背包网格中每个位置的值。这里我们依然可以给出一个计算最长公共子串网格每个位置的值的公式。
这个公式的伪代码如下:
if word_a[i] == word_b[j]: #两个字母相同
cell[i][j] = cell[i-1][j-1] + 1
else: #两个字母不同
cell[i][j] = 0
#大白话讲即是 字母相同时等于斜左上方网格的值加1,不同的话则为0
3、小结
这里给了两个例子来浅显的探讨如何理解动态规划方法,而对于动态规划方法的使用:
- 需要在给定约束条件下优化某种指标时,动态规划很有用。
- 问题可分解为离散子问题时,可使用动态规划来解决。
- 每种动态规划解决方案都涉及网格。
- 单元格中的值通常就是你要优化的值。
- 每个单元格都是一个子问题,因此你需要考虑如何将问题分解为子问题。
- 没有放之四海皆准的计算动态规划解决方案的公式,这也是为什么在这里放两个例子的原因,另外想要更好的理解动态规划,必须要手动画格子,画格子,画格子。
4、背包问题的python 动态规划实现代码
def KnapsackProblem(row, col):
# initialize the grid
grid = [[0]+[1500]*col] + [[0, 1500]+[None]*(col-1) for _ in range(len(row)-1)]
for i in range(1, len(row)):
for j in range(1, col+1):
if j >= goods[i][0]:
grid[i][j] = max(grid[i-1][j], row[i][1]+grid[i-1][j-row[i][0]])
else:
grid[i][j] = grid[i-1][j]
return grid
raw_goods = [(1, 1500), (4, 3000), (3, 2000), (4, 5000)]
# format goods
goods = {i: raw_goods[i] for i in range(len(raw_goods))}
Knapsack = 4
print(KnapsackProblem(row=goods, col=Knapsack))
print(goods)
参考:算法图解