《演算法導論》動態規劃 -------python 鋼條切割問題
1. 鋼條切割問題:一段長為 n 的鋼條和一個價格表 pi (i=1,2,3,4,...,n),求切割方案,使得銷售收益 Rn 最大。
長度 i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
價格 pi | 1 | 5 | 8 | 9 | 10 | 17 | 17 | 20 | 24 | 30 |
長度為 n 的鋼條有 2^(n-1) 種切割方案(因為在距離鋼條左端i(i=1,2,,3,...,n-1)處,我們總是可以選擇切割或者不切割)
設一個最優切割方案將鋼條切為 k (1≤k≤n)段,最優切割方案為:n = i1 + i2 + i3 +...+ ik
切割的長度分別為:i1、i2 、i3 、...、ik,得到的最大收益:Rn = pi1 + pi2 + pi3 +...+ pik
2. 問題分析
首先可以將鋼條分割為 i 和 n-i 兩段,求這兩段的最優收益 Ri 和 R(n-i) (每種方案的最優收益為兩段的最優收益之和)。由於無法確定哪種方案(i 取何值時)會獲得最優收益,我們必須考慮所有的i,選取其中收益最大者。若直接出售鋼條會獲得最大收益,可以選擇不做任何切割。最優切割收益公式:Rn = max(pn,R1+Rn-1,R2+Rn-2,...,Rn-1+R1)。當完成首次切割後,我們可以將兩段鋼條( i
一種相似但更為簡單的遞迴求解方法:將長度為 n 的鋼條分解為左邊開始一段,以及剩餘部分繼續分解的結果。簡化後的公式:Rn = max{1≤i≤n, (pi+Rn-i)}
3. 程式設計
方法1:自頂向下遞迴實現
缺點:輸入規模大時,程式執行時間會變得相當長
原因:反覆求解相同的子問題
# 自頂向下遞迴演算法實現 # R[n] = max{1≤i≤n, (pi+R[n-i])} 時間複雜度為:2^n(指數函式) def CutRod(p, n): # 函式返回:切割長度為 n 的鋼條所得的最大收益 if n == 0: return 0 q = -1 for i in range(1, n+1): q = max(q, p[i] + CutRod(p, n-i)) ''' tmp = p[i] + CutRod(p, n-i) if q < tmp: q = tmp ''' return q p=[0,1,5,8,9,10,17,17,20,24,30] # 價格表,下標為對應的鋼條長度,如當鋼條長度為0時,p=0,即p[0]=0,p[2]=5 print("最大收益為:",CutRod(p,4)) # 最大收益為: 10
假如 n=4時,求解函式用C(4)表示,下圖為 n=4 時 函式的遞迴展開,其中被淺綠色和黃色虛線方框圈住的部分為重複的部分。其中共有2^n個結點,2^(n-1)葉節點,程式時間複雜度為2^n(指數時間複雜度)
方法2:帶備忘錄的自頂向下遞迴
特點:對每個子問題只求解一次,並將結果儲存下來(隨後再次遇到此問題的解,只需查詢儲存的結果,不必重新計算),用記憶體空間來節省計算時間。
# 帶備忘錄的自頂向下法 -- 每個子問題只求解一次,並將之存放在陣列 r 中,以備用
def MemorizedCutRod(p, n):
r=[-1]*(n+1) # 陣列初始化
def MemorizedCutRodAux(p, n, r):
if r[n] >= 0:
return r[n]
q = -1
if n == 0:
q = 0
else:
for i in range(1, n + 1):
q = max(q, p[i] + MemorizedCutRodAux(p, n - i, r))
r[n] = q
return q
return MemorizedCutRodAux(p, n, r),r
print("最大收益為:",MemorizedCutRod(p, 5)) # 最大收益為: (13, [0, 1, 5, 8, 10, 13])
方法3:自底向上法(動態規劃)
特點:任何子問題的求解都依賴於 更小子問題 的求解。對子問題規模進行排序,按由小到大的問題進行求解。當求解某個子問題時,他所依賴的更小的子問題都已求解完畢,結果已經儲存。
優點:時間複雜度在三種方法中最好
# 自底向上
def BottomUpCutRod(p, n):
r = [0]*(n+1)
for i in range(1, n+1):
if n == 0:
return 0
q =0
for j in range(1, i+1):
q = max(q, p[j]+r[i-j])
r[i] = q
return r[n],r
p=[0,1,5,8,9,10,17,17,20,24,30]
print(BottomUpCutRod(p, 10)) # (30, [0, 1, 5, 8, 10, 13, 17, 18, 22, 25, 30])