動態規劃指的是一個問題能夠拆分紅多個小的最優子問題,而且這些子問題具備重疊,典型的如斐波那契數列:f(2)=f(1)+f(0),f(3)=f(2)+f(1),f(4)=f(3)+f(2),若使用簡單的遞歸算法求f(4),則f(2)會被計算兩次,當計算f(n)時須要計算f(n-1)和f(n-2)而f(n-1)又要計算記一次f(n-2),如此往復時間複雜度爲n的指數級別,致使算法效率低下。若可以記錄f(2)至f(n-1)的結果,能夠保證每一級計算都是O(1)複雜度,整個算法的時間複雜度就能降低至O(n),空間複雜度O(n)。必須保證拆分後的子問題是當前規模下的最優解,纔可保證遞歸邏輯的正確,典型的例子是無權最短路徑問題,若已知A到除最終目的地B外的全部點最短路徑,則只需遍歷尋找與B直接相鄰全部點到A最近的一個。算法
一般動態規劃能夠分爲4個步驟:spa
所以,動態規劃的關鍵是分析子問題的拆分與遞歸式。下面四個問題來自《算法導論》第三版。code
有一條長度爲n的鋼條,能夠不計成本的切割成多條鋼條出售,不一樣長度與價格關係以下表所示,求如何切割得到最大的利益rnorm
長度iblog |
1遞歸 |
2ci |
3it |
4table |
5ast |
6 |
7 |
8 |
9 |
10 |
價格pi |
1 |
5 |
8 |
9 |
10 |
17 |
17 |
20 |
24 |
30 |
以長度n=4爲例,分割共有如下幾種方案
n=4, r=9
n=1+3, r=9
n=1+1+2, r=7
n=1+1+1+1, r=4
n=2+2, r=10
最佳方案爲分紅2+2兩端,利潤爲10
對於長度爲n的鋼條,其能夠經過切割得到的最大利益記爲rn,rn=max(pn,r1+rn-1,r2+rn-2,...rn-1+r1) rn的最大利潤可能有兩種狀況:不切割或者先切爲兩段,該兩段各自的ri+rn-i爲最大值。所以能夠採用遞歸的方式,求出rn的值,僞代碼以下:
1 int cutRod(p,n){ 2 if(n==0) 3 return 0; 4 q=-1 5 for(i=1;i<=n;i++){ 6 q=max(q,p[i]+cutRod(p,n-i)); 7 } 8 return q; 9 }
該算法的問題是效率過低,緣由在於cutRod(p,i)這個值在不一樣階段被分別計算了屢次,好比要求長度爲2的鋼條的最大利益,要計算分割成1+1的利益,這裏r1被計算了兩次。若是可以記錄r1到rn-1的值,能夠大幅度提交計算效率,這是一種典型的空間換取時間的方法——動態規劃算法。
動態規劃有兩種等價的實現方法:
第一種,帶備忘的自頂向下法。在之間遞歸算法調用每一層的時候,先檢查該值有沒有被計算過,若沒有,調用並存儲;若計算過,直接取出該值。僞代碼以下:
1 int memoizedCutRod(p,n){ 2 r[n+1] //用於記錄r0到rn-1的值 3 for(i=0;i<=n;i++){ 4 r[i]=-1; 5 } 6 return memoizedCutRodAux(p,n,r); 7 } 8 9 int memoizedCutRodAux(p,n,r){ 10 if(r[n]>=0) 11 return r[n]; 12 if(n==0) 13 q=0; 14 else 15 { 16 q=-1; 17 for(i=0;i<=n;i++){ 18 q=max(q,p[i]+ memoizedCutRodAux(p,n-i,r)); 19 } 20 } 21 r[n]=q; 22 return q; 23 }
第二種,自底向上法,將一個問題分紅規模更小的子問題,從小到大進行求解,當求解至原問題時,所需的值都已求解完畢。對於分割鐵棒問題來講,從長度爲1一直求解至長度爲n時最佳分割方案的收益。因爲rn=max(pn,r1+rn-1,r2+rn-2,...rn-1+r1),只需計算出r1至rn-1的值,即可計算出rn的值。僞代碼以下:
1 int bottomUpCutRod(p,n){ 2 r[n+1] //用於記錄r0到rn-1的值 3 s[n+1]//若要輸出分割長度,則須要記錄不一樣長度最大利潤的分割狀況 4 r[0]=0; 5 for(j=1;j<=n;j++){ 6 q=-1; 7 for(i=1;i<=j;i++){ 8 //針對長度爲j時,遍歷全部的分割狀況,尋找到最佳的結果 9 if(p[i]+r[j-i]>q){ 10 q= p[i]+r[j-i]; 11 s[j]=i;//記錄分割位置 12 } 13 } 14 r[j]=q; 15 } 16 return r[n]; 17 } 18 void printCutRod(s,n){ 19 if(s[n]!=0) 20 printCutRod(s,s[n]); 21 printf("%d ",s[n]);//此處輸出的爲全部的分割位置 22 }
矩陣相乘是符合結合律的, A1A2A3= A1(A2A3),可是二者的計算規模多是不一樣的。假設三個矩陣的大小分別是 10*100、100*五、 5*50,則 A1A2A3的計算次數爲 10*100*5+10*5*50=7500,而 A1(A2A3)的計算規模爲 100*5*50+10*100*50=75000,二者相差了 10 倍的規模。對於一組給定的矩陣相乘A1A2A3 ⋯ An要求出如何進行乘法結合能夠進行最少的計算次數。
下面使用形如A1∗n來表示A1A2A3 ⋯ An的最終乘積結果。對於A1∗n的最少計算式,其一定在Ak 處進行了分割 (A1A2A3 ⋯ Ak)(Ak+1Ak+2 ⋯ An),總計算次數爲 m,i, j- = min{m,i, k- +m,k + 1, j- + pi−1pkpj}, i ≤ k < j,故必需要先求出A1A2A3 ⋯ Ak和Ak+1Ak+2 ⋯ An各自的最少計算次數而後遍歷計算出最小值。 所以能夠採用自 1 int matrixChainOrder(p){
2 n=p.length-1; 3 m[n][n],s[n][n];//m記錄矩陣鏈各自的最少計算次數,s記錄最少時分割位置 4 for(i=0;i<=n;i++) 5 m[i][i]=0; 6 for(l=2;l<=n;l++){//l限制矩陣鏈的長度,先計算出全部2個矩陣相乘的最少次數,而後是3個矩陣,直至n個矩陣 7 for(i=1;i<=n-l+1;i++){ 8 j=i+l-1; 9 m[i][j]=INFI; 10 // m[i,j]=min{m[i,k]+m[k+1,j]+p_(i-1) p_k p_j },i≤k<j 11 for(k=i;k<=j-1;k++){ 12 q=m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j]; 13 if(q<m[i][j]){ 14 m[i][j]=q; 15 s[i][j]=k; 16 } 17 } 18 } 19 } 20 //此處可添加打印分割結果的代碼 21 return m[n]; 22對於X = x1x2 ⋯ xm和Y = y1y2 ⋯ yn的 LCS 記爲Z = z1z2 ⋯ zk最優子結構拆分:
1. 若是xm = yn,則zk = xm = yn且Zk−1是Xm−1和Yn−1的一個 LCS
2. 若是xm ≠ yn且zk ≠ xm,則 Z 是Xm−1和 Y 的一個 LCS
3. 若是xm ≠ yn且zk ≠ yn,則 Z 是 X 和Yn−1的一個 LCS
用 c[i, j] 表 示XiYj 的 LCS 長 度 , 可 列 出 遞 歸 式
1 int lcsLength(X,Y){ 2 m=X.length; 3 n=Y.length; 4 b[m][n];//記錄最優解的構造 5 c[m+1][n+1]; 6 for(i=1;i<=m;i++) 7 c[i][0]-0; 8 for(j=0;j<=n;j++) 9 c[0][j]=0; 10 for(i=1;i<=m;i++){ 11 for(j=1;j<=n;j++){ 12 if(x[i]==y[i]){ 13 c[i][j]=c[i-1][j-1]+1; 14 b[i][j]="↖"; 15 } 16 else if(c[i-1][j]>=c[i][j-1]){ 17 c[i][j]=c[i-1][j]; 18 b[i][j]="↑"; 19 } 20 else{ 21 c[i][j]=c[i][j-1]; 22 b[i][j]="←"; 23 } 24 } 25 } 26 printLcs(b,X,m,n); 27 return c[n]; 28 } 29 30 void printLcs(b,X,i,j){ 31 if(i==0||j==0) 32 return; 33 if(b[i][j]=="↖"){ 34 printLcs(b,X,i-1,j-1); 35 print x[i]; 36 return; 37 } 38 if(b[i][j]=="↑"){ 39 printLcs(b,X,i-1,j); 40 return; 41 } 42 printLcs(b,X,i,j-1); 43 }
對於搜索樹來講,不一樣節點的搜索頻率是不一樣的,節點離根越遠搜索時間就越長,因此咱們但願將搜索頻率高的節點放在離根近的位置,使得總體的效率指望值最優。可是,並非簡單地把搜索頻率最高的點作根節點就好了,其他節點的深度增長反而可能致使總體效率下降,極端狀況最小值搜索頻率最高,若做爲根節點,整棵樹的平衡性不好,反而容易致使搜索效率的下降。
對於一個二叉搜索樹,有n個關鍵字k1,k2,...,kn和n+1個僞關鍵字d0,d1,d2,...dn,其中d0表明小於k1的搜索結果,d1是大於k1小於k2的搜索結果,搜索k是成功的搜索,而搜索d是失敗的搜索,因此d必定是葉子節點且di和di-1必定是ki的兩個子節點。對這樣節點的最優二叉搜索樹來講,他含有根節點和兩棵子樹,包含連續的關鍵字ki,ki+1,...,kj和對應的僞關鍵字,該子樹一定是對應規模的最優二叉搜索樹,不然只需將該規模下的最優二叉搜索樹替換該子樹就會產生搜索指望值更小的樹,這與最優二叉樹的假設矛盾。對於特殊狀況j=i-1時,樹不包含實際關鍵字,僅含有僞關鍵字di-1。p爲關鍵字的搜過幾率,q爲僞關鍵字的搜索機率,對於通常狀況,須要從ki,ki+1,...,kj中選擇根節點kr來構造最優二叉搜索樹。當該樹成爲目標結果的子樹時,因在子樹的指望值基礎上增長全部點的機率之和,由於每一個點的深度都增長了1。對於給點的節點條件,只需尋找到使左右子樹的指望值加上全部節點機率之和最小即爲最優二叉搜索樹(左右子樹全部節點加上根節點的權是1),所以能夠對指望搜索代價列出遞歸式
1 double optimal-bst(p,q,n){ 2 e[n+2][n+1];//e[1..n+1][0..n]記錄指望值 3 w[n+2][n+1];//w[1..n][0..n]記錄i到j的機率和避免重複計算 4 root[n+1][n+1];//root[1..n][1..n]記錄全部樹的根節點 5 for(i=1;i<=n+1;i++){ 6 //初始化j=i-1的特殊狀況 7 e[i][i-1]=q[i-1]; 8 w[i][i-1]=q[i-1]; 9 } 10 for(l=1;l<=n;l++){//l表示關鍵字的個數,先計算1個實際關鍵字的樹,而後2個依次增長 11 for(i=1;i<=n-l+1;i++){ 12 j=i+l-1; 13 e[i][j]=INFI; 14 w[i][j]=w[i][j-1]+p[i]+q[j];//計算w[i][j] 15 for(r=i;r<=j;r++){ 16 t=e[i][r-1]+e[r+1][j]+w[i][j];//計算e[i,r-1]+e[r+1,j]+w(i,j) 17 if(t<e[i][j]){ 18 e[i][j]=t; 19 root[i][j]=r; 20 } 21 } 22 } 23 } 24 //省略了打印代碼 25 return e[1][n]; 26 }