動態規劃(Dynamic Programming)與分治方法類似,都是經過組合子問題的解來求解原問題。不一樣的是,分治方法一般將問題劃分爲互不相交的子問題,遞歸地求解子問題,再講它們的解組合起來,求出原問題的解。而動態規劃應用於子問題重疊的狀況,即不用的子問題具備公共的子子問題。在這種狀況下,若是採用分治算法,則分治算法會作許多沒必要要的工做,它會反覆地求解那些公共子子問題。對於動態規劃法,它對每一個子子問題只求解一次,將其保存在一個表格中,從而無需每次求解一個子子問題時都從新計算,避免了這種沒必要要的計算工做。
也就是說,動態規劃法與分治方法相比,是用空間來換時間,而時間上得到的效益是很客觀的,這是一種典型的時空平衡(time-memory trade-off)的策略。一般,動態規劃法用來求解最優化問題(optimization problem),如斐波那契數列求值問題,鋼條切割問題,0-1揹包問題,矩陣鏈乘法問題,最長公共子序列(LCS)問題,最優二叉搜索樹問題等。
通常狀況下,動態規劃算法的步驟以下:算法
接下來,咱們將從斐波那契數列求值這個簡單的例子入手,來分析動態規劃法的具體步驟和優勢。app
斐波那契數列記爲$\{f(n)\}$,其表達式以下:函數
$$ \left\{ \begin{array}{lr} f(0)=0\\ f(1)=1\\ f(n)=f(n-1)+f(n-2),n\geq 2 \end{array} \right. $$優化
具體寫出前幾項,就是:0,1,1,2,3,5,8,13,21,34,55,89,144,233......
接下來,咱們將會採用遞歸法和動態規劃法來求解該數列的第n項,即f(n)的值。spa
首先,咱們採用遞歸法來求解斐波那契數列的第n項$f(n)$,其算法描述以下:code
function fib(n) if n = 0 return 0 if n = 1 return 1 return fib(n − 1) + fib(n − 2)
分析上述僞代碼,先是定義一個函數fib(n),用來計算斐波那契數列的第n項,當$n\geq 2$時,它的返回值會調用函數fib(n-1)和fib(n-2).當$n=5$時,計算fib(5)的函數調用狀況以下圖所示:blog
在計算fib(5)時,fib(5)調用1次,fib(4)調用1次,fib(3)調用2次,fib(2)調用3次,fib(1)調用5次,fib(0)調用3次,一共調用函數fib()15次。由此,咱們能夠看到,在計算fib(5)時,存在屢次重複的fib()函數的調用,當n增大時,重複調用的次數會急劇增長,如計算fib(50)時,fib(1)和fib(0)大約會被調用$2.4\times10^{10}$次。因而可知,該算法的效率並非很高,由於該算法的運行時間是指數時間。
咱們用Python實現上述算法,並計算f(38)的值及運算時間。Python代碼以下:遞歸
import time # recursive method def rec_fib(n): if n <= 1: return n else: return rec_fib(n-1) + rec_fib(n-2) # time cost of cursive method t1 = time.time() t = rec_fib(38) t2 = time.time() print('結果:%s, 運行時間:%s'%(t, t2-t1))
輸出結果以下:內存
結果:39088169, 運行時間:22.93831205368042
在使用遞歸法來求解斐波那契數列的第n項時,咱們看到了遞歸法的不足之處,由於遞歸法在使用過程當中存在大量重複的函數調用,所以,效率不好,運行時間爲指數時間。爲了解決遞歸法存在的問題,咱們能夠嘗試動態規劃法,由於動態規劃法會在運行過程當中,保存上一個子問題的解,從而避免了重複求解子問題。對於求解斐波那契數列的第n項,咱們在使用動態規劃法時,須要保存f(n-1)和f(n-2)的值,犧牲一點內存,可是能夠顯著地提高運行效率。
動態規劃法來求解斐波那契數列第n項的僞代碼以下:ci
function fib(n) var previousFib := 0, currentFib := 1 if n = 0 return 0 else if n = 1 return 1 repeat n−1 times var newFib := previousFib + currentFib previousFib := currentFib currentFib := newFib return currentFib
在上述僞代碼中,並無存在重複求解問題,只是在每次運行過程當中,保存上兩項的值,再利用公式$f(n)=f(n-1)+f(n-2)$來求解第n項的值。用Python實現上述過程,代碼以下:
import time # bottom up approach of Dynamic Programming def dp_fib(n): previousFib = 0 currentFib = 1 if n <= 1: return n # repeat n-1 times for _ in range(n-1): newFib = previousFib + currentFib previousFib = currentFib currentFib = newFib return currentFib # time cost of DP method t1 = time.time() t = dp_fib(38) t2 = time.time() print('結果:%s, 運行時間:%s'%(t, t2-t1))
輸出結果以下:
結果:39088169, 運行時間:0.0
顯然,使用動態規劃法來求解斐波那契數列第n項的運行效率是很高的,由於,該算法的時間複雜度爲多項式時間。
用遞歸法和動態規劃法來求解該數列的第n項,完整的Python代碼以下:
# calculate nth item of Fibonacci Sequence import time # recursive method def rec_fib(n): if n <= 1: return n else: return rec_fib(n-1) + rec_fib(n-2) # bottom up approach of Dynamic Programming def dp_fib(n): previousFib = 0 currentFib = 1 if n <= 1: return n # repeat n-1 times for _ in range(n-1): newFib = previousFib + currentFib previousFib = currentFib currentFib = newFib return currentFib # time cost of cursive method t1 = time.time() t = rec_fib(38) t2 = time.time() print('結果:%s, 運行時間:%s'%(t, t2-t1)) # time cose of DP method s = dp_fib(38) t3 = time.time() print('結果:%s, 運行時間:%s'%(t, t3-t2))
輸出結果以下:
結果:39088169, 運行時間:22.42628264427185 結果:39088169, 運行時間:0.0