演算法思想之動態規劃
動態規劃一直被認為是最難理解的一種演算法思想,什麼重疊子問題、動態轉移方程、最優子結構等等,一聽就高深莫測,沒有往下學習下去的動力
一、初識動態規劃
廢話不多說,我們直接先上一個經典的例子。那就是耳熟能詳的斐波那契數列問題。我們先來看一下問題的定義。
斐波那契數列的定義如下: 斐波那契數列指的是這樣一個數列0,1,1,2,3,5,8,13,21,34,55,89,144,..... 它以遞迴的方法來定義: F(0)=0,F(1)=1, F(n)=F(n-1)+F(n-2)(n>=2,n∈N*)
- 遞迴解決:
這個例子最直觀的方法就是用遞迴的方式來實現,畢竟斐波那契數列是用遞迴來定義的。我們來看一下程式碼實現。
def fibs(n): if n<2: return n return fibs(n-1)+fibs(n-2)
C#實現
public static int Foo(int i) { if (i <= 0) { return 0; } else if (i > 0 && i <= 2) { return 1; }else { return Foo(i - 1) + Foo(i - 2); } }
這樣是不是很簡單。我們接下來看一下呼叫的遞迴樹。我們以fibs(6)為例。
其中每個結點表示要計算的斐波那契數列的第幾項,我們可以從上圖發現,會出現許多重複計算的問題,比如fib(4)就計算了兩次。這樣就會帶來時間和空間上的消耗,那我們有什麼方式可以避免重複計算的問題。我們可以使用遞迴中的“備忘錄”功能來解決。我們來看一下程式碼如何實現。
二、用動態規劃解決
我們把整個求解過程分為n個階段,每個階段去求解數列對應項的值。我們在解決當前問題時,也就是求解該對應項的值的時候,會依賴過去的狀態,也就是前面幾項的值來計算。比如我們在求解fibs(6)的時候,我們需要用到fibs(5)和fibs(4)這兩項。 我們來定義一個數組,來記錄每項的狀態。我們也叫做狀態轉移矩陣。 按照斐波那契數列的定義:
F(0)=0,F(1)=1 F(n)=F(n-1)+F(n-2) (n>=2)
我們可以看到F(n)的值只與他的前兩個狀態有關。所以我們只要知道他的前兩個狀態,就可以求出F(n)。
- 初始化值F(0)=0,F(1)=1,我們直接放入陣列中。
- 要想計算F(2),我們需要知道F(0)和F(1),因為上一步已經放入陣列中,我們直接拿來用就好了,然後把F(2)的結果放入陣列中。
- 要想計算F(3),我們需要知道F(2)和F(1),因為F(2)和F(1)已經存在數組裡了,我們直接拿來用就好了,然後把F(3)的結果放入陣列中。
....
依此類推,知道計算到n為止。整個狀態轉移矩陣就計算好了。如下圖所示。我們以求解F(5)為例。
下面我們直接看程式碼實現,這樣比較簡單明瞭。
def fibs(n): if n<2: return n dp=[0 for _ in range(n+1)] dp[0]=0 dp[1]=1 for i in range(2,n+1): dp[i]=dp[i-1]+dp[i-2] return dp[n] print(fibs(6))
上面的程式碼是不是很簡潔明瞭。這就是一種用動態規劃來解決問題的思路。我們把問題分解為n個階段,一個階段一個階段去求解。然後通過當前狀態,來求出下一個狀態,動態的往前推進,這是不是還挺形象的