動態規劃
動態規劃思想:將原問題拆解成若干子問題,同時保存子問題的答案,使得每個子問題只求解一次,最終獲得原問題的答案。
求解思路
- 大多數動態規劃問題都是一個遞歸問題;
- 在遞歸的過程中會發現很多重疊子問題(出現重複計算子問題的情況);
- 可以使用記憶化搜索的方式來解決問題;
- 通常解決動態規劃問題時,先自頂向下的思考問題,最後再通過自底向上的動態規劃解決問題。
在動態規劃中都是通過這種思路來解決問題的。
下面以求解斐波那契數列來說明上面的過程。
斐波那契數列
遞歸問題
這個式子可以很容易的寫成遞歸形式。
def fib(n):
if n == 0:
return 0
if n == 1:
return 1
return fib(n-1) + fib(n-2)
遞歸形式的思路雖然簡單,但是這段代碼的耗時是指數增長的。
在jupyter上求fib(30)
就耗時256毫秒。
重疊子問題
下面我們來看下爲什麼計算這麼慢。
如果我們想要計算fib(5)
,那麼根據定義,我們要計算fib(4)
和fib(3)
。
如果要計算4,那麼就要計算3和2。以此類推,我們可以畫出整個計算斐波那契數列的遞歸樹。
在這個遞歸樹種,每個葉子節點都到了1或0的終止條件。這裏每個節點都是一次計算。
從上圖可以發現,我們進行了多次的重複計算。
拿求解fib(2)
來說,我們計算了3次。
記憶化搜索-自頂向下的解決問題
對於這些重複計算,是否可以只計算一次呢。很簡單的思路是用一個數據結構將之前的計算結果保存起來。
memo = {} #保存計算結果
def fib(n):
if n == 0:
return 0
if n == 1:
return 1
if n not in memo: # 如果沒有計算過再去計算
memo[n] = fib(n-1) + fib(n-2)
return memo[n]
這種方式就是記憶化搜索。我們使用了遞歸搜索的方式,但是使用了memo
進行記憶。
現在計算起來就非常快了。
fib(1000)
也可以計算了。
記憶化搜索的實質是在遞歸的基礎上加上記憶化的過程。
遞歸是一種自頂向下的解決問題。也就是說,我們沒有從最基本的問題開始解決,而是假設最基本的問題已經解決了。我們已經會求fib(n-1)
和fib(n-2)
了,那麼求fib(n)
就是把它們加起來就好了。
通常如果我們能自頂向下的解決問題的話,我們也能自底向上的解決問題。
動態規劃-自底向上的解決問題
memo = {}
def fib(n):
memo = {0:0,1:1} #memo中存的就是第i個斐波那契數列
for i in range(2,n+1):
memo[i] = memo[i-1] + memo[i-2]
return memo[n]
這樣的一個過程就是自底向上的解決問題,我們先解決小數據量上的結果(i
是從2
開始的),然後層層遞推來解決更大的數據量的問題。
這樣的過程就是動態規劃。
通常解決動態規劃問題時,先自頂向下的思考問題,最後再通過自底向上的動態規劃解決問題。