1. 程式人生 > >以計算斐波那契數列為例說說動態規劃算法(Dynamic Programming Algorithm Overlapping subproblems Optimal substructure Memoization Tabulation)

以計算斐波那契數列為例說說動態規劃算法(Dynamic Programming Algorithm Overlapping subproblems Optimal substructure Memoization Tabulation)

ash 麻省理工學院 遞歸樹 經典 top 有關 ctu dynamic 代碼

動態規劃(Dynamic Programming)是求解決策過程(decision process)最優化的數學方法。它的名字和動態沒有關系,是Richard Bellman為了唬人而取的。

動態規劃主要用於解決包含重疊子問題的最優化問題,其基本策略是將原問題分解為相似的子問題,通過求解並保存重復子問題的解,然後逐步合並成為原問題的解。動態規劃的關鍵是用記憶法儲存重復問題的答案,避免重復求解,以空間換取時間。

用動態規劃解決的經典問題有:最短路徑(shortest path),0-1背包問題(Knapsack problem),旅行商人問題(traveling sales person)等等。

(註:背包問題分為兩種:若物體不可分割,則稱為0-1背包問題,比如拿一塊金磚;若物體可以分開,則稱為一般背包問題,比如拿多少克大米。一般背包問題可以用貪心算法解決。貪心算法在每個階段即可找出當前最優解,每個階段的最優狀態都是由上一個階段的最優狀態得到的。)

可以采用動態規劃來求解的問題需要具有以下兩個主要特征:

1)重疊子問題(Overlapping Subproblems):有些子問題會被重復計算多次。

2)最優子結構(Optimal Substructure):問題的最優解可以從某個子問題的最優解中獲得。

下面以計算斐波那契數列為例,看看動態規劃算法的實現過程。

以下是1-5的斐波那契數列遞歸樹:

                         fib(5)
                     /                            fib(4)                fib(3)
             /      \                /              fib(3)      fib(2)         fib(2)    fib(1)
        /     \        /    \       /      fib(2)   fib(1)  fib(1) fib(0) fib(1) fib(0)
  /    fib(1) fib(0)

可以看出,fib(5)是由fib(4)和fib(3)相加而成,fib(4)則是由fib(3)和fib(2)相加而成,等等。其中,fib(3)要計算2次,fib(2)要計算3次。這裏面進行了很多重復的計算。

按之前博客中提到的遞歸方法來計算這個斐波那契數列(用遞歸方法計算斐波那契數列),在此基礎上加入print("fib called with",n)語句後,看看fib函數的調用情況:

def fib(n):
    print("fib called with",n)  #看調用了哪個fib函數,也就是說看計算了斐波那契數列的第幾項
    if n<2:
        return n
    else:
        return (fib(n-1) + fib(n-2))

計算一下斐波那契數列的第5項試試:

print(fib(5))

運行結果如下:

fib called with 5
fib called with 4
fib called with 3
fib called with 2
fib called with 1
fib called with 0
fib called with 1
fib called with 2
fib called with 1
fib called with 0
fib called with 3
fib called with 2
fib called with 1
fib called with 0
fib called with 1
5

可以看出一共進行了15次調用,其中fib(3)被計算了2次,fib(2)被計算了3次。

而使用動態規劃算法來計算這個斐波那契數列,運行則會快一些。代碼如下:

def fastFib(n,memo):  #memo是設置的一個字典
    print("fib1 called with",n)
    if not n in memo:  #如果斐波那契數列的第n項數值不在字典裏,那麽用遞歸方式計算該值,並把該值放入字典中
        memo[n]=fastFib(n-1,memo)+fastFib(n-2,memo)
    return memo[n]   #如果斐波那契數列的第n項數值在字典裏,那麽直接返回字典裏的該項數值

def fib1(n):
    memo={0:0,1:1}  #初始化一個字典
    return fastFib(n,memo)

同樣也計算一下斐波那契數列的第5項試試,運行結果如下:

fib1 called with 5
fib1 called with 4
fib1 called with 3
fib1 called with 2
fib1 called with 1
fib1 called with 0
fib1 called with 1
fib1 called with 2
fib1 called with 3
5

可以看出一共進行了9次調用,在進行過一次計算之後,後面的調用都是直接到字典裏去獲取該值即可。

具體來說,以上用動態規劃算法來計算斐波那契數列的過程是這樣的:首先設置一個空數組,當需要子問題的解時,先去這個數組中查找。如果此問題之前已經求過解,那麽就直接返回該值,如果此問題之前並未求過解,那麽就計算該值並把結果放入數組中,以備後用。

有兩種不同的方式來存儲這些數值:

1) 默記法(從上到下)/ Memoization (Top Down)

2) 表格法(從下到上)/ Tabulation (Bottom Up)

那麽到底應該用默記法還是表格法呢?

如果需要求解所有的子問題,那麽表格法往往要比默記法好。這是因為表格法沒有遞歸的額外消耗,並且使用預先分配好的數組(preallocated array),而不是哈希圖(hash map)。

如果只是需要求解其中一些子問題,那麽默記法則要好些。

參考:麻省理工學院公開課:計算機科學及編程導論(第13集)

以計算斐波那契數列為例說說動態規劃算法(Dynamic Programming Algorithm Overlapping subproblems Optimal substructure Memoization Tabulation)