原做者:金子冴
校阅:内野良一
翻译:叶子
原文连接
动态规划是一种解题手法的总称。它经过将一个没法解决的大问题分解成复数个小问题(也叫子问题),而后在解决这些小问题的基础之上来解决原始的大问题。经过使用动态规划,咱们能将一部分在多项式时间内没法解决的问题,在相似多项式的时间内求得最优解(稍后会进行说明)。
判断一个问题是否能够经过动态规划来解决的时,咱们须要判断该问题是否知足可分治(分而治之)和可记忆(将阶段性成果进行缓存,便于重复利用)两个条件。首先,让咱们先去理解:多项式时间、分而治之、以及记忆化(Memoization)。python
多项式时间是指由多项式表示的计算时间。多项式时间算法是指当入力的大小(长度或者个数)是n的时候,计算时间(执行步数)的上限在n的多项式时间内可以表示的算法。好比,计算九九乘法表的算法的计算时间能够表示为9x9。将其扩展到nxn的时候,计算时间用大O记法来表示的话,能够表示为O(n2)。这代表该算法的计算时间的上限能够用n2来表示,所以计算nxn的乘法的算法能够说是多项式算法。
可是,在多项式时间内没法解决的问题也是存在的,好比说接下来将要说明的最短路径问题,在多项式时间内就没法解决。以下图所示的加权路线图,找一个从START开始到到达GOAL的花费最短(权重最小)的路线。算法
为了求最短路线,咱们须要考虑所有路线的排列组合,在此基础之上进行花费的计算,要使得花费最小,那就须要找到最短的路径。像这样的问题,入力的规模每增大一点,路线的组合就呈指数级增长,所以计算所有路线的花费是不现实的。可是,若是使用了动态规划,就能够求得相似最短路径这样的在多项式时间内没法解决的问题的最优解。计算时会使用分而治之和记忆化两种手法。缓存
分治指的是将目标问题分割成复数个子问题的手法。让咱们试着将刚才提到的最短路径问题进行子问题分解。对于刚才提到的例子,首先不要去考虑从START开始可以到达END的全部路线,而应该只考虑在某个时间点可以推动的路线。因此对于最开始的路线,只须要考虑START到a,b,c,d这四条。考虑到咱们要解决的是最短路径的问题,这里咱们选择从START开始花费最小的START->b路线开始。接着,咱们只需考虑从b点出发可以推动的路线,这里咱们也是选择花费最少的路线,b->g路线。优化
像这样,将一个须要考虑所有路径的问题转换为只考虑某个时间点可以推动的路线的问题(子问题)的分治手法,叫作分而治之。spa
记忆化是指将计算结果保存到内存上,以后再次利用的手法。做为解释记忆化的例子,让咱们来思考一下斐波那契数列的问题。这里咱们省略斐波那契数列数列的说明。使用python进行斐波那契数列计算的场合,代码编写以下所示:翻译
清单13d
CulcFibonacci.pycode
import sys # フィボナッチ数の計算 def culc_fibonacci(n): if n > 1: return culc_fibonacci(n-1) + culc_fibonacci(n-2) elif n == 1: return 1 else: return 0 def main(): # 1~10番目フィボナッチ数列を表示 # ⇒ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 for n in range(10): fibonacci_n = culc_fibonacci(n) print(fibonacci_n, end='') if not n == 9: print(', ', end='') if __name__ == '__main__': main() sys.exit(0)
可是,清单1所示代码,在计算n=10的时候,必须去计算n=9~1,所以计算时间是O(αn:α的n次幂)(α:实数),因此当n变大的时候,相关的计算量会呈指数级增加。
下图表示的是斐波那契数列的计算过程。从下图咱们能够看出,除了f(10)以外的全部计算都不止一次。blog
将清单所示代码用记忆化进行优化的时候,如何减小复数次计算是重点。为了进行记忆化,咱们须要作一个记忆化表,将第一次计算的值存储到该表之中。递归
这样,当咱们须要再次计算某个值的时候,直接去该表当中查询以前计算过得值便可。这样就防止了进行屡次一样的计算。
以下所示清单2的源代码,对清单1的源代码进行了记忆化优化。
清单2
CulcFibonacciMemo.py
import sys # メモ化テーブル(辞書形式) fibonacci_list = {} # フィボナッチ数の計算(メモ化あり) def culc_fibonacci_memo(n): global fibonacci_list if n == 1: return 1 elif n == 0: return 0 if not n in fibonacci_list: fibonacci_list[n] = culc_fibonacci_memo(n-1) + culc_fibonacci_memo(n-2) return fibonacci_list[n] def main(): # 1~10番目フィボナッチ数列を表示 # ⇒ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 for n in range(10): fibonacci_n = culc_fibonacci_memo(n) print(fibonacci_n, end='') if not n == 9: print(', ', end='') if __name__ == '__main__': main() sys.exit(0)
记忆化的最大优势是经过减小计算量,从而减小了计算的时间。清单2所示代码会将第一次计算的斐波那契数存储起来,以后经过再次利用以前的计算结果来减小计算量。实际上,笔者在本身的PC上计算f(40)的斐波那契数的时候,清单1没有进行记忆化优化的程序用了101.9秒,而清单2进行了记忆化优化的程序只用了0.2秒,二者的计算时间相比,后者的计算时间大幅度缩减。因为动态规划是以递归的方式计算子问题,所以这种存储优化很是重要。
对于动态规划的概要说明到此为止,接下来的章节咱们将尝试用Dijkstra算法(动态规划的一种)来解决最短路径的问题。
下一节将介绍用Dijkstra的方法解决最短路径问题(Python实现)。