0. 概念
將原問題拆解成若干子問題,同時保存子問題的答案,使得每個子問題只求解一次,最終獲得原問題的答案。
這個概念聽起來跟帶記憶的遞歸(即記憶化搜索)是一樣的,其實本質上就是相同的,如果要分清楚的話可以參考下圖。都是用於解決遞歸問題,且都能夠避免重複計算重疊子問題,不同之處在於記憶化搜索是自頂向下解決問題的,而動態規劃是自底向上解決問題的。但是有時候會將這兩種形式都歸爲動態規劃。
1. 算法設計步驟
(1)刻畫一個最優解的結構特徵
(2)遞歸地定義最優解的值
(3)計算最優解的值,通常採用自底向上的方法
(4) 利用計算出的信息構造一個最優解
2.斐波那契數列
在遞歸和帶記憶的遞歸(原理和例子)這篇博文中我們討論了計算斐波那契數列的普通遞歸方法和加了記憶的遞歸方法(即記憶化搜索),這裏接着討論如何使用動態規劃來計算斐波那契數列:
def fibonacci(n):
memo = [-1]*(n+1)
memo[0] = 0
memo[1] = 1
for i in range(2,n+1):
memo[i] = memo[i-1]+memo[i-2]
return memo[n]
print(fibonacci(6))
結果爲:8
從計算斐波那契數列的例子中就可以感受到記憶化搜索是先假定已知,然後計算的時候再一層一層往下計算,而使用動態規劃是先從和開始一點一點往求,這就是自頂向下和自底向上的區別。
3. 鋼條切割
Serling公司購買長鋼條,將其切割爲短鋼條出售。切割工序本身沒有成本支出。公司管理層希望知道最佳的切割方案。假定我們知道Serling公司出售一段長爲i英寸的鋼條的價格爲(i=1,2,…,單位爲美元)。鋼條的長度均爲整英寸。圖15-1給出了一個價格表的樣例。
鋼條切割問題是這樣的:給定一段長度爲n英寸的鋼條和一個價格表(i=1,2,…n),求切割鋼條方案,使得銷售收益最大。注意,如果長度爲n英寸的鋼條的價格足夠大,最優解可能就是完全不需要切割。
(1)普通遞歸
分析:
如果我們切下來的第一段長度爲,則收益爲加上剩餘長度鋼條切割所得的收益,也就是說這個問題與它的子問題的形式是一樣的,所以可以想到能使用遞歸的方法來解決。
def cut_rod(p, n):
if n == 0:
return 0
q = -1
for i in range(1, n+1):
q = max(q, p[i]+cut_rod(p, n-i))
return q
p = {1:1, 2:5, 3:8, 4:9, 5:10, 6:17, 7:17, 8:20, 9:24, 10:30}
for i in range(1, 11):
n = i
result = cut_rod(p, n)
print(result)
結果爲:
1
5
8
10
13
17
18
22
25
30
(2)記憶搜索
設定一個記憶單元,對每一個長度鋼條的最高收益做一個記錄,從而避免重複計算。
def cut_rod(p, n, memo):
if n == 0:
return 0
if memo[n] >= 0:
return memo[n]
q = -1
for i in range(1, n+1):
q = max(q, p[i]+cut_rod(p, n-i, memo))
memo[n] = q
return q
p = {1: 1, 2: 5, 3: 8, 4: 9, 5: 10, 6: 17, 7: 17, 8: 20, 9: 24, 10: 30}
for i in range(1, 11):
n = i
memo = [-1]*(n+1)
result = cut_rod(p, n, memo)
print(result)
結果爲:
1
5
8
10
13
17
18
22
25
30
(3) 動態規劃
不管是普通遞歸還是記憶搜索都是自頂向下地解決問題,這裏用自底向上的思路來解決一下。動態規劃的思路是先將是的最優收益表示出來並記住,然後基於這個再計算時的最優收益,依次往下直到計算出時的最優收益。
def cut_rod(p, n):
r = [-1]*(n+1)
r[0] = 0
for i in range(1, n+1):
q = -1
for j in range(1, i+1):
q = max(q, p[j]+r[i-j])
r[i] = q
return r[n]
p = {1: 1, 2: 5, 3: 8, 4: 9, 5: 10, 6: 17, 7: 17, 8: 20, 9: 24, 10: 30}
for i in range(1, 11):
n = i
result = cut_rod(p, n)
print(result)
結果爲:
1
5
8
10
13
17
18
22
25
30