1. 程式人生 > >演算法導論-動態規劃

演算法導論-動態規劃

動態規劃演算法

動態規劃(dynamic programming)是通過組合子問題來求解原問題的方法,它應用於解決子問題重疊的情況,即不同子問題具有公共的子問題。
通常動態規劃可以按照如下四個步驟進行設計:
1.刻畫一個最優解的結構特徵;
2.遞迴地定義最優解的值;
3.計算最優解的值,通常採用自底向上的方法;
4.利用計算出的資訊構造一個最優解(按照要求,可有可無)。

一、鋼條切割問題

在這裡插入圖片描述在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述
自頂向下遞迴實現

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

用Python實現程式碼:

def value_max(p, n):
    if n == 0:
        return 0
    q = 0
    for i in range(0, n):
        q = max(q, p[i] + value_max(p, n - i - 1))
    return q

price= [1, 5, 8, 9, 10, 17, 17, 20, 24, 30]
print (value_max(price, 8))

對於長度為n的鋼條,CUT-ROD顯然考察了所有2^(n-1)種可能的切割方案。樸素遞迴演算法之所以效率很低,是因為它反覆求解相同的子問題。
動態規劃方法是付出額外的記憶體空間來節省計算時間,是典型的時空權衡的例子。而時間上的節省可能是非常巨大的:可能將一份指數時間的解轉化為一個多項式時間的解。
動態規劃有兩種等價的實現方法
第一種是帶備忘錄的自頂向下法:

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=i to n
		q=max(q,p[i]+MEMOIZED-CUT-ROD-AUX(p,n-i,r))
		r[n]=q
		return q

用Python寫得程式碼為:

def cutMemo(p,n):
    r=[0]*(n+1)
    def value_max(p,n,r):
        if n==0:
            return 0
        q=0
        for i in range(0,n):
            q=max(q,p[i]+value_max(p,n-i-1,r))
            r[i]=q
        print (r)
        return q
    return value_max(p,n,r)
price=[1,5,8,9,10,17,17,20,24,30]
print (cutMemo(price,8))

第二種是自底向上法:

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]

用Python編寫的程式碼如下:

def cutMemo(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-1]+r[i-j])
            r[i]=q
    return r
price=[1,5,8,9,10,17,17,20,24,30]
print (cutMemo(price,8))

子問題圖

它是一個有向圖,每個定點唯一地對應一個子問題。若求子問題x的最優解時需要直接用到子問題y的最優解,那麼在子問題圖中就會有一條子問題x的頂點到子問題y的頂點的有向邊。我們可以將子問題圖看做自頂向下遞迴呼叫樹的“簡化版”或“收縮版”,因為書中所對應相同子問題的結點合併為圖中的單一頂點,相關的所有邊都從父結點指向子節點。
子問題圖G=(V,E)的規模可以幫助我們確定動態規劃演算法的執行時間。由於每個子問題只求解一次,因此演算法執行時間內等於每個子問題求解時間之和。

二、矩陣鏈乘法

我們稱有如下性質的矩陣乘積鏈為完全括號化的:它是一個單一矩陣,或者是兩個完全括號化的矩陣乘積鏈的積,且已外加括號。
兩個矩陣相乘的標準演算法的虛擬碼:

MATRIX-MULTIPLY(A,B)
if A.columns≠B.rows
	error "incompatible dimensions"
else let C be a new A.rows*B.columns matrix
	for i=1 to A.rows
		for j=1 to B.columns
		Cij=0
		for k=1 to A.columns
			Cij=Cij+Aik*Bkj
return C

兩個矩陣A和B只有相容,即A的列數等於B的行數時,才能相乘。
矩陣鏈乘法問題:給定n個矩陣的鏈<A1,A2,…,An>,矩陣Ai的規模為Pi-1*Pi(1<=i<=n),求完全括號花方案,使得計算乘積A1A2…An所需的乘法次數最少。
計算AiAi+1…Aj最小代價括號化方案的遞迴求解公式為:
在這裡插入圖片描述
對矩陣鏈AiAi+1…Aj最優括號化的子問題的虛擬碼為:

MATRIX-CHAIN-ORDER(p)
n=p.length-1
let m[1..n,1..n] and s[1..n-1,2..n] be new tables
for i=1 to n
	m[i,i]=0
for l=2 to n
	for i=1 to n-l+1
		j=i+l-1
		m[i,j]=∞
		for k=i to j-1
			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]=q
return m and s
輸出最優括號化方案的虛擬碼:
PRINT-OPTIMAL-PARENS(s,i,j)
if i==j
	print "A"i
else print "("
	PRINT-OPTIMAL-PARENS(s,i,s[i,j])
	PRINT-OPTIMAL-PARENS(s,s[i,j]+1,j)
	print ")"	

用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):
        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 ()
    PRINT_OPTIMAL_PARENS(s,1,n-1)
    print ()

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='')

A=[5,10,3,12,5,50,6]
MATRIX_CHAIN_ORDER(A)

動態規劃原理

適合應用動態規劃辦法求解的最優化問題應該具備的兩個要素:最優子結構和子問題重疊。
無權最短路徑:找到一條從u到v的邊數最少的路徑。無權最短路徑問題具有最優子結構性質。
無權最長路徑:找到一條從u到v的邊數最多的簡單路徑。無權最長路徑問題不具有最優子結構性質。此問題是NP完全的。
簡單路徑:路徑上的各頂點均不互相重複。
最長簡單路徑問題的子結構與最短路徑差別大的原因是:雖然最長路徑問題和最短路徑問題的解都用到了兩個子問題,但兩個最長簡單路徑子問題是相關的,而兩個最短路徑子問題是無關的。子問題無關的含義是,同一個原問題的一個子問題的解不影響另一個子問題的解。

三、最長公共子序列

給定一個序列X=(x1,x2…xm),另一個序列Z=(z1,z2…zk)滿足如下條件時稱為X的子序列,即存在一個嚴格遞增的X的下標序列(i1,i2,…,ik),對所有j=1,2,…k,滿足x_i_j=z_j。
在這裡插入圖片描述
計算最長公共子序列的虛擬碼:

LCS-LENGTH(X,Y)
m=X.length
n=Y.length
let b[1..m,1..n] and c[0..m,0..n]be new tables
for i=1 to m
	c[i,0]=0
for j=0 to n
	c[0,j]=0
for i=1 to m
	for j=1 to n
		if xi==yi
			c[i,j]=c[i-1,j-1]+1
			b[i,j]="↖"
		elseif c[i-1,j]≥c[i,j-1]
					c[i,j]=c[i-1,j]
					b[i,j]="↑"
		else c[i,j]=c[i,j-1]
				b[i,j]="←"
return c and b

用Python編寫的程式碼:

def LCS_LENGTH(X, Y):
    m= len(X)
    n = len(Y)
    c = [[0 for i in range(n + 1)] for j in range(m + 1)]
    flag = [[0 for i in range(n + 1)] for j in range(m + 1)]
    for i in range(m):
        for j in range(n):
            if X[i] == Y[j]:
                c[i + 1][j + 1] = c[i][j] + 1
                flag[i + 1][j + 1] = 'ok'
            elif c[i + 1][j] > c[i][j + 1]:
                c[i + 1][j + 1] = c[i + 1][j]
                flag[i + 1][j + 1] = 'left'
            else:
                c[i + 1][j + 1] = c[i][j + 1]
                flag[i + 1][j + 1] = 'up'
    return c, flag


def PRINT_LCS(flag, X, i, j):
    if i == 0 or j == 0:
        return
    if flag[i][j] == 'ok':
        PRINT_LCS(flag, X, i - 1, j - 1)
        print(X[i - 1], end='')
    elif flag[i][j] == 'left':
        PRINT_LCS(flag, X, i, j - 1)
    else:
        PRINT_LCS(flag, X, i - 1, j)
X = 'ABCBDAB'
Y = 'BDCABA'
c, flag = LCS_LENGTH(X, Y)
for i in c:
    print(i)
print('')
for j in flag:
    print(j)
print('')
PRINT_LCS(flag, X, len(X), len(Y))
print('')

四、最優二叉搜尋樹

最優二叉搜尋樹不一定是高度最矮的樹。而且概率最高的關鍵字也不一定出現在二叉搜尋樹的根結點。
在這裡插入圖片描述
最優二叉搜尋樹的期望搜尋代價為e[i,j],對於包含關鍵樹Ki,…,Kj的子樹,所有概率之和為:
在這裡插入圖片描述
求解最優二叉樹搜尋的虛擬碼:

OPTIMAL-BST(p,q,n)
let e[1..n+1,0..n],w[1..n+1,0..n],and root[1..n,1..n]be new tables
for i=1 to n+1
	e[i,i-1]=q_i-1
	w[i,i-1]=q_i-1
for l=1 to n
	for i=1 to n-l+1
		j=i+l-1
		e[i,j]=∞
		w[i,j]=w[i,j-1]+p_i+q_j
		for r=i to j
			t=e[i,r-1]+e[r+1,j]+w[i,j]
			if t<e[i,j]
				e[i,j]=t
				root[i,j]=r
return e and root