原做者:金子冴
校閱:內野良一
翻譯:葉子
原文連接
動態規劃是一種解題手法的總稱。它經過將一個沒法解決的大問題分解成複數個小問題(也叫子問題),而後在解決這些小問題的基礎之上來解決原始的大問題。經過使用動態規劃,咱們能將一部分在多項式時間內沒法解決的問題,在相似多項式的時間內求得最優解(稍後會進行說明)。
判斷一個問題是否能夠經過動態規劃來解決的時,咱們須要判斷該問題是否知足可分治(分而治之)和可記憶(將階段性成果進行緩存,便於重複利用)兩個條件。首先,讓咱們先去理解:多項式時間、分而治之、以及記憶化(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實現)。