一、動態規划要點
1 最優子結構性質
當問題的最優解包含了其子問題的最優解時,稱該問題具有最優子結構性質。
2 重疊子問題性質
動態規划算法對每個問題只解一次,將其解保存在一個表格中,當再次需要解此問題時,用常數時間查看一下結果。因此,用動態規划算法通常只需要多項式時間。
二、備忘錄方法要點
備忘錄方法:
1 用一個表格來保存已解決的子問題的答案,用的時候查表即可。
2 采用的遞歸方式是自頂向下。
3 控制結構與直接遞歸相同,區別在於備忘錄方式為每個解過的子問題建立備忘錄。
4 初始化為每個子問題的記錄存入一個特殊的值,表示並未求解。在求解過程中,查看相應記錄如果是特殊值,表示未求解,否則只要取出該子問題的解答即可。
三、動態規划和備忘錄方法的區別
1、動態規划是自低向上 ,備忘錄方法是自頂向下。
2、動態規划每個子問題都要解一次,但不會求解重復子問題;備忘錄方法只解哪些確實需要解的子問題。
四、矩陣連乘問題
動態規划四個步驟
1. 找出最優解的性質,並且刻畫其結構特性;
2. 遞歸的定義最優解;
3. 以自底向上的方式刻畫最優值;
4. 根據計算最優值時候得到的信息,構造最優解
一般來講,當一個問題的所有子問題都至少要解一次時,使用動態規划算法比使用備忘錄方法好。此時,動態規划算法沒有任何多余的計算。同時,對於許多問題,常常可以利用其規則的表格存取方式,減少動態規划算法的計算時間和空間需求。當子問題空間中的部分子問題可以不必求解時,使用備忘錄方法則較為有利,因為從其控制結構可以看出,該方法只解那些確實需要求解的問題。
對於動態規划算法,我們必須明確兩個基本要素,這兩個要素對於在設計求解具體問題的算法時,是否選擇動態規划算法具有指導意義:
1 算法有效性依賴於問題本身所具有的最優子結構性質:設計算法的第一步通常是要刻畫最優解的結構。當問題的最優解包含了子問題的最優解時,稱該問題具有最優子結構性質。問題的最優子結構性質提供了該問題可以使用動態規划算法求解的重要線索
在矩陣連乘積最優次序問題中注意到,若A1A2...An的最優完全加括號方式在Ak和Ak+1之間斷開,則由此可以確定的子鏈A1A2A3...Ak和Ak+1Ak+2...An的完全加括號方式也最優,即該問題具有最優子結構性質。在分析該問題的最優子結構性質時候,所使用的方法具有普遍性。首先假設由原問題導出的子問題的借不是最優解,然后在設法說明在這個假設下可以構造出比原問題最優解更好的解,從而導致矛盾。
在動態規划算法中,利用問題的最優子結構性質,以自底向上的方式遞歸的從子問題的最優解逐漸構造出整個問題的最優解。算法考察的子問題的空間規模較小。例如在舉證連乘積的最優計算次序問題中,子問題空間由矩陣鏈的所有不用的子鏈組成。所有不用的子鏈的個數為o(n*n),因而子問題的空間規模為o(n*n)
2 可以用動態規划算法求解問題應該具備另一個基本要素是子問題的重疊性。在用遞歸算法自頂向下求解此問題時候,每次產生的子問題並不總是新問題,有些子問題被反復計算多次。動態規划算法正是利用了這種子問題的重疊性質,對每一個子問題都只是求解一次,而后將其保存到一個表格中,當再次需要解此問題時,只是簡單使用常數時間查看一下結果。通常,不同子問題個數隨着問題大小呈多項式增長。因此使用動態規划算法通常只是需要多項式時間,從而獲得較高的解題效率。
下面是使用動態規划算法求解矩陣連乘問題的Java實現:
1 package dynamic_planning; 2 3 public class Strassen { 4 /* 5 * array[i][j]表示Ai...Aj相乘最少計算次數 6 * s[i][j]=k,表示Ai...Aj這(j-i+1)個矩陣中最優子結構為Ai...Ak和A(k+1)...Aj 7 * p[i]表示Ai的行數,p[i+1]表示Ai的列數 8 */ 9 private int array[][]; 10 private int p[]; 11 private int s[][]; 12 13 public Strassen(){ 14 p=new int[]{2,4,5,5,3}; 15 array=new int[4][4]; 16 s=new int[4][4]; 17 } 18 19 public Strassen(int n,int []p){ 20 this.p=new int[n+1]; 21 this.array=new int[n][n]; 22 this.s=new int[4][4]; 23 for(int i=0;i<p.length;i++) 24 this.p[i]=p[i]; 25 } 26 /*********************方法一,動態規划**********************************/ 27 public void martixChain(){ 28 int n=array.length; 29 for(int i=0;i<n;i++) 30 array[i][i]=0; 31 for(int r=2;r<=n;r++){ 32 for(int i=0;i<=n-r;i++){ 33 int j=i+r-1; 34 array[i][j]=array[i+1][j]+p[i]*p[i+1]*p[j+1]; 35 s[i][j]=i; 36 for(int k=i+1;k<j;k++){ 37 int t=array[i][k]+array[k+1][j]+p[i]*p[k+1]*p[j]; 38 if(t<array[i][j]){ 39 array[i][j]=t; 40 s[i][j]=k; 41 } 42 } 43 } 44 } 45 } 46 /* 47 * 如果待求矩陣為:Ap...Aq,then a=0,b=q-p 48 */ 49 public void traceBack(int a,int b){ 50 if(a<b){ 51 traceBack(a, s[a][b]); 52 traceBack(s[a][b]+1, b); 53 System.out.println("先把A"+a+"到A"+s[a][b]+"括起來,在把A"+(s[a][b]+1)+"到A"+b+"括起來,然后把A"+a+"到A"+b+"括起來"); 54 } 55 } 56 57 /*********************方法二:備忘錄方法*****************************/ 58 public int memorizedMatrixChain(){ 59 int n=array.length; 60 for(int i=0;i<n;i++){ 61 for(int j=i;j<n;j++) 62 array[i][j]=0; 63 } 64 return lookUpChain(0,n-1); 65 } 66 67 public int lookUpChain(int a,int b){ 68 if(array[a][b]!=0) 69 return array[a][b]; 70 if(a==b) 71 return 0; 72 array[a][b]=lookUpChain(a, a)+lookUpChain(a+1, b)+p[a]*p[a+1]*p[b+1]; 73 s[a][b]=a; 74 for(int k=a+1;k<b;k++){ 75 int t=lookUpChain(a, k)+lookUpChain(k+1, b)+p[a]*p[k+1]*p[b+1]; 76 if(t<array[a][b]){ 77 array[a][b]=t; 78 s[a][b]=k; 79 } 80 } 81 return array[a][b]; 82 } 83 public static void main(String[] args) { 84 Strassen strassen=new Strassen(); 85 //strassen.martixChain(); 86 strassen.memorizedMatrixChain(); 87 strassen.traceBack(0, 3); 88 } 89 }
五、最長公共子序列問題LCS
1、公共子序列
子序列:給定一個序列X=<x1,x2…,xm>,另一個序列Z=<z1,z2…,zk>,存在一個嚴格遞增的X的下標序列<i1,i2…ik>,滿足對所有的j=1,2,…,k,xij = zj
公共子序列:給定兩個序列X和Y,Z同時是X和Y的子序列,稱Z是X和Y的公共子序列。
2、LCS的最優子結構
令X=<x1,x2…,xm>和Y=<y1,y2,…,ym>為兩個序列,Z=<z1,z2…,zk>為X和Y的任意LCS
(1) 如果xm=yn,那么zk=xm=yn且Zk-1是Xm-1Yn-1的一個LCS
(2) 如果xm≠yn,那么zk≠xm意味着Z是Xm-1和Y的一個LCS
(3) 如果xm≠yn,那么zk≠yn意味着Z是X和Yn-1的一個LCS
3、遞歸解
c[i,j]是Xi和Yj的LCS長度
c[i,j]
= 0 , i=0且j=0
= c[i-1,j-1] ,i,j>0且xi=yi
= max(c[i,j-1],c[i-1,j]), i,j>0且xi≠yj
4、例子
X:ABCBDAB Y:BDCABA => LCS:BCBA
動態規划算法:聲明了一個m+1*n+1大小的table,0行0列初始化為0,然后從左往右,從上往下地按照遞歸解填表。填表結束后通過Print可以遞歸地按同樣的方法打印出所求的子序列。
1 LCS-LENGTH(X,Y) 2 m=X.length+1 3 n=Y.length+1 4 let c[0..m,0..n] be new table 5 for i=0 to m 6 c[i,0] = 0 7 for j=0 to n 8 c[0,j] = 0 9 for i=0 to m 10 for j=1 to n 11 if x[i]==y[j] 12 c[i,j]=c[i-1,j-1]+1 13 else if c[i-1,j]>=c[i,j-1] 14 c[i,j]=c[i-1,j] 15 else c[i,j]=c[i,j-1] 16 return c 17 18 PRINT-LCS(c,X,i,j) 19 if i==0 and j==0 20 return 21 if c[i,j]==c[i-1,j-1]+1 22 PRINT-LCS(c,X,i-1,j-1) 23 print x[i] 24 else if c[i,j]=c[i-1,j] 25 PRINT-LCS(c,X,i-1,j) 26 else 27 PRINT-LCS(c,X,i,j-1)
備忘錄方法:用於將table初始化,0行0列都為0,其他元素都為-1,表示備忘錄沒有記錄這些元素,初始化完成調用LCS_RECUR函數。
LCS_RECUR函數:如果備忘錄已經有記錄直接返回,否則遞歸地查表並回填備忘錄的當前位置。
1 LCS_MEMORIZED(c, X, Y) 2 m=X.length+1 3 n=Y.length+1 4 let c[0..m,0..n] be new table 5 for i=0 to m 6 c[i,0] = 0 7 for j=0 to n 8 c[0,j] = 0 9 for i=0 to m 10 for j=0 to n 11 c[i,j] = -1 12 return LCS_RECUR(c,X,Y,m,n) 13 14 LCS_RECUR(c,X,Y,i,j) 15 if c[I,j] ≠ -1 16 return c[I,j] 17 if x[i-1]==y[j-1] 18 lcs = LCS_RECUR(c,x,y,i-1,j-1)+1 19 else 20 up = LCS_RECUR(c,x,y,i-1,j) 21 left = LCS_RECUR(c,x,y,i,j-1) 22 lcs = max(up, left) 23 c[i,j] = lcs 24 return lcs
對比:
方法比較
1、動態規划法是自底向上,僅通過迭代就可以完成,觀察C表的填充順序,是從上至下從左至右依次填充。當填充完畢后可以得到任意位置(任意子序列)的LCS。
備忘錄法是自頂到底,遞歸地填充C表,僅會填充計算當前LCS會用到的項,所以當執行完畢后還有許多元素尚未填充,意味着只能得到部分子序列的LCS。
2、時間復雜度
兩種方法的時間復雜度都是O(mn),因為都需要維護C[m][n]的表。如果更細致地比較備忘錄法需要填充的元素較少,填充過程的代價是O(max(m,n)),而動態規划法填充過程的代價是O(mn),但是實際上備忘錄法初始化復雜度是O(mn),並且遞歸調用也會在常數項上增加時間代價。所以兩種方法實際的執行速度差別不大。
3、空間復雜度:O(mn)