《算法導論》這門課的老師是黃劉生和張曙,兩位都是老人家了,代課很慢很沒有激情,不過這一章非常有意思。更多見:iii.run
前言:
書中列舉四個常見問題,分析如何采用動態規劃方法進行解決。
裝配線調度問題
矩陣鏈乘問題:
最長公共子序列問題:
最優二叉查找樹問題:
基本概念
動態規劃通常應用于最優化問題,此類問題可能包含多個可行解。每個解有一個值,而我們期望找到最大或者最小的解。
動態規劃算法的設計可以分為以下4個步驟:
- 描述最優解的結構。
- 遞歸定義最優解的值。
- 按自底向上的方式計算最優解的值。(其實還應該有自頂向下的求解)
- 由計算出的結果構造一個最優解。
動態規劃算法效率會非常高的原因在于,其特殊的實現方法,也就是第三步。
兩種等價的實現方法:
- 帶備忘的自頂向下法,此方法按照正常的遞歸編寫過程,但過程會保存每個子問題的解(通常保存在一個數組或散列表中)。當需要一個子問題的解時,程序首先檢查是否已經保存過此解。如果是,直接返回;若不是,遞歸計算這個子問題。這種遞歸方式是帶備忘的。
- 自底而上法,這種方法需要恰當的定義子問題的規模,因為任何一個子問題的求解的依賴著更小的子問題。因此需要將問題進行排序,從小的問題開始處理。這樣可以確保,在處理到大的問題時,其所依賴的更小的子問題已經求解完畢,結果已經保存。
因此,我們會說 動態規劃算法屬于典型的用空間換取時間。由于沒有頻繁的遞歸函數的開銷,自底而上方法的時間復雜度會更好一些。
動態規劃與分治法之間的區別:
- 分治法是指將問題分成一些獨立的子問題,遞歸的求解各子問題。
- 動態規劃適用于這些子問題不是獨立的情況,也就是各子問題包含公共子問題。
動態規劃基礎
什么時候可以使用動態規范方法解決問題呢?這個問題需要討論一下,書中給出了采用動態規范方法的最優化問題中的兩個要素:最優子結構和重疊子結構。
最優子結構(自下向上)
最優子結構是指問題的一個最優解中包含了其子問題的最優解。在動態規劃中,每次采用子問題的最優解來構造問題的一個最優解。
尋找最優子結構,遵循的共同的模式:
問題的一個解可以是做一個選擇,得到一個或者多個有待解決的子問題。
假設對一個給定的問題,已知的是一個可以導致最優解的選擇,不必關心如何確定這個選擇。
在已知這個選擇后,要確定哪些子問題會隨之發生,如何最好地描述所得到的子問題空間。
利用“剪貼”技術,來證明問題的一個最優解中,使用的子問題的解本身也是最優的。
最優子結構在問題域中以兩種方式變化:
- 有多少個子問題被使用在原問題的一個最優解中。
- 在決定一個最優解中使用哪些子問題時有多少個選擇。
動態規劃按照自底向上的策略利用最優子結構,即:首先找到子問題的最優解,解決子問題,然后逐步向上找到問題的一個最優解。
為了描述子問題空間,可以遵循這樣一條有效的經驗規則,就是盡量保持這個空間簡單,然后在需要時再擴充它。
注意:在不能應用最優子結構的時候,就一定不能假設它能夠應用。 警惕使用動態規劃去解決缺乏最優子結構的問題!
重疊子問題(自上向下)
用來解決原問題的遞歸算法可以反復地解同樣的子問題,而不是總是產生新的子問題。
重疊子問題是指當一個遞歸算法不斷地調用同一個問題。
動態規劃算法總是充分利用重疊子問題,通過每個子問題只解一次,把解保存在一個需要時就可以查看的表中,每次查表的時間為常數。
由計算出的結果反向構造一個最優解:把動態規劃或者是遞歸過程中作出的每一次選擇(記住:保存的是每次作出的選擇)都保存下來,在最后就一定可以通過這些保存的選擇來反向構造出最優解。
做備忘錄的遞歸方法:這種方法是動態規劃的一個變形,它本質上與動態規劃是一樣的,但是比動態規劃更好理解!
(1) 使用普通的遞歸結構,自上而下的解決問題。
(2) 當在遞歸算法的執行中每一次遇到一個子問題時,就計算它的解并填入一個表中。以后每次遇到該子問題時,只要查看并返回表中先前填入的值即可。
鋼條切割問題
題目
給定一個長度為n的鋼條,以及一個價格表p,p中列出了每英寸鋼條的價格,將長度為n的鋼條切割為若干短鋼條出售,求一個鋼條的切割方案,使得收益最大,切割工序沒有成本。比如價格表p如下:
長度為n的鋼條,一共有$2^{n-1}$種不同的切割方案,因為可以再距離鋼條左邊為i(i=1,2,…,n-1)處,我們總是可以選擇切割或者不切割。比如下圖表示了n=4的切割情況:
理論依據
我們稱鋼條切割問題滿足最優子結構性質:問題的最優解由相關子問題的最優解組合而成,而這些子問題可以獨立求解。
我們可以這樣理解鋼條問題:將鋼條從左邊切下一段長度為i的一段,對剩下的n-i的部分繼續進行切割(遞歸求解),而不對左邊長度為i的一段在進行切割。
$r_n = \mathop{max}\limits_{1 \leq i \leq n} (p_i + r_{n-i})$
這樣問題的解就轉化為最優解了。
自頂向下遞歸實現
CUT-ROD(p,n)
if n == 0
return 0
q = -∞
for i = 1 to n
q = max(q, p[i]+CUT-ROD(p, n-i))
return q
CUT-ROD的效率很差,這是因為CUT-ROD反復的求解一些相同的子問題,下圖顯示了當n==4時的調用情況:
帶備忘的自頂向下
MEMOIZED-CUT-ROD(p,n)
let r[0..n] be a new array
for i = 0 to n
r[i]= -∞
return MEMOIZED-CUT-ROD-AUX(p, n, r)
MEMOIZED-CUT-ROD-AUX(p,n, r)
if r[n] >= 0
return r[n]
if n == 0
q = 0
else q = -∞
for i = 1 to n
q= max(q, p[i]+MEMOIZED-CUT-ROD-AUX(p, n-i,r))
r[n] = q
return q
該方法與之前的普通遞歸方法類似,只是會在過程中保存子問題的解,當需要一個子問題的解的時候,先查看是否已經保存過了,如果是,則直接使用即可。否則,按常規的遞歸方式計算子問題。所以稱為帶備忘的,因為它記住了之前已經計算出的結果。
自底向上的方法
BOTTOM-UP-CUT-ROD(p,n)
let r[0..n] be a new array
r[0]= 0
for j = 1 to n
q= -∞
for i = 1 to j
q = max(q, p[i]+r[j-i])
r[j]= q
return r[n]
方法采用子問題的自然順序,因此過程中依次求解規模為$j=0,1,2,3,4...,n$的問題。
這兩種算法具有相同的時間復雜度,BOTTOM-UP-CUT-ROD主要是雙層嵌套循環,所以時間復雜度$Θ(n2)$。MEMOIZED-CUT-ROD的時間復雜度也是$Θ(n2)$。可以使用子問題圖進行分析。
python實現切割鋼條問題
def cut_rod():
p = [0,1,5,8,9,10,17,17,20,24,30]
n = len(p)
r = [0 for i in range(n)]
s = [0 for i in range(n)]
for j in range(n):
q = -10
for i in range(j+1):
if q < (p[i]+r[j-i]):
q = (p[i]+r[j-i])
s[j] = i
#q = max(q,p[i]+r[j-i])
r[j] = q
def find_way(n):
cut_rod()
print(n,'--->',r[n])
while n > 0:
print(s[n])
n = n - s[n]
調用find_way(9)
輸出
矩陣鏈乘法
題目
給定n 個矩陣的序列,希望求它們的乘積:$A_1A_2A_3...A_n$ 。因為矩陣的乘法滿足結合律,所以可以對n個矩陣序列加括號,來改變乘積順序。比如對于矩陣鏈< $A_1$, $A_2$,$A_3$,$A_4$>可以有下面的加括號方案:
不同的加括號的方案,對于乘積運算的代價影響很大.
兩個矩陣相乘,A為p * q矩陣,B為q * r矩陣。所以A 的乘法次數為pqr。
如果A(10,100 ),B(100,5), C(5,50 )三個矩陣相乘。
如果按照((AB)C)的順序,則需要101005 + 10550 = 7500次乘法運算,如果按照(A(BC))的順序,則需要100550 + 1010050 = 75000次乘法運算。所以,不同的加括號方案,對于矩陣鏈乘法的代價影響很大。
解題步驟
刻畫最優解的結構特征
通過尋找最優子結構,利用最優子結構從子問題的最優解中構造出原問題的最優解。
假設$A_iA_{i+1}...A_j$的最優括號花方案的分割點是在$A_k$和$A_{K+1}$之間,一個非平凡的矩陣鏈乘法任何時候都是需要劃分鏈的,任何最優解都是有子問題的最優解構成的。
遞歸的定義最優解的值
令$m[i,j]$表示計算矩陣$A_{i,j}$所需標量乘法的最小值,也即原問題的最優解,計算$A_{1..n}$的最低代價就是$m[1,n]$。
- 對于i == j 的平凡問題,矩陣鏈只包含唯一的矩陣$A_{i,j}$。
- 對于$A_iA_{i+1}...A_j$的最優括號化方案的切割點在$A_k$和$A_{k+1}$之間。那么$m[i,j]$的解相當于計算$A_{i..k}$和$A_{k+1..j}$的代價加上,合并這兩個子答案所需要的代價$p_{i-1}p_k p_j$
因此,我們得到
$$m[i,j] = m[i,k]+m[k,j]+p_{i-1}p_k p_j$$
計算最優解的值
算法應當按照長度遞增的順序求解矩陣鏈括號化問題,并按照對應的順序填寫表m。對舉證連$A_{i,j}$,其規模為鏈的長度j-i+1
偽代碼就不寫了,直接寫python代碼
python實現矩陣鏈乘法問題
def MATRIX_CHAIN_ORDER(p):
n = len(p)
s = [[0 for j in range(n)] for i in range(n)]
m = [[0 for j in range(n)] for i in range(n)]
for l in range(2, n): #l is the chain length
for i in range(1, n-l+1):
j = i + l - 1
m[i][j] = 1e9
for k in range(i, j):
q = m[i][k] + m[k+1][j] + p[i-1]*p[k]*p[j]
if q < m[i][j]:
m[i][j] = q
s[i][j] = k
PRINT_OPTIMAL_PARENS(s, 1, n-1)
return m
def PRINT_OPTIMAL_PARENS(s, i, j):
if i == j:
print('A', end = '')
print(i, end = '')
else:
print('(', end = '')
PRINT_OPTIMAL_PARENS(s, i, s[i][j])
PRINT_OPTIMAL_PARENS(s, s[i][j]+1, j)
print(')', end = '')
if __name__ == "__main__":
A = [30, 35, 15, 5, 10, 20,25]
m = MATRIX_CHAIN_ORDER(A)
print('\n','共計需要',m[1][n-1],'次相乘')
以上