動態規劃法(一)從斐波那契數列談起

動態規劃法與分治方法

  動態規劃(Dynamic Programming)與分治方法類似,都是經過組合子問題的解來求解原問題。不一樣的是,分治方法一般將問題劃分爲互不相交子問題遞歸地求解子問題,再講它們的解組合起來,求出原問題的解。而動態規劃應用於子問題重疊的狀況,即不用的子問題具備公共的子子問題。在這種狀況下,若是採用分治算法,則分治算法會作許多沒必要要的工做,它會反覆地求解那些公共子子問題。對於動態規劃法,它對每一個子子問題只求解一次,將其保存在一個表格中,從而無需每次求解一個子子問題時都從新計算,避免了這種沒必要要的計算工做。
  也就是說,動態規劃法與分治方法相比,是用空間來換時間,而時間上得到的效益是很客觀的,這是一種典型的時空平衡(time-memory trade-off)的策略。一般,動態規劃法用來求解最優化問題(optimization problem),如斐波那契數列求值問題,鋼條切割問題,0-1揹包問題,矩陣鏈乘法問題,最長公共子序列(LCS)問題,最優二叉搜索樹問題等。
  通常狀況下,動態規劃算法的步驟以下:算法

  1. 刻畫一個最優解的結構特徵。
  2. 遞歸地定義最優解的值。
  3. 計算最優解的值,一般採用自底向上的方法。
  4. 利用計算出的信息構造一個最優解。

  接下來,咱們將從斐波那契數列求值這個簡單的例子入手,來分析動態規劃法的具體步驟和優勢。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項的運行效率是很高的,由於,該算法的時間複雜度爲多項式時間。

參考文獻

  1. 算法導論(第四版)
  2. https://www.cs.upc.edu/~jordi...
  3. https://www.saylor.org/site/w...

附錄

用遞歸法和動態規劃法來求解該數列的第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
相關文章
相關標籤/搜索