[toc]ios
動態規劃通常也只能應用於有最優子結構的問題。最優子結構的意思是局部最優解能決定全局最優解(對有些問題這個要求並不能徹底知足,故有時須要引入必定的近似)。簡單地說,問題可以分解成子問題來解決。c++
dynamic programming is a method for solving a complex problem by breaking it down into a collection of simpler subproblems.web
適合採用動態規劃方法的最優化問題的倆個要素:最優子結構性質,和子問題重疊性質。算法
最優子結構:若是問題的最優解所包含的子問題的解也是最優的,咱們就稱該問題具備最優子結構性質(即知足最優化原理)。意思就是,總問題包含不少個子問題,而這些子問題的解也是最優的。數組
重疊子問題:子問題重疊性質是指在用遞歸算法自頂向下對問題進行求解時,每次產生的子問題並不老是新問題,有些子問題會被重複計算屢次。動態規劃算法正是利用了 這種子問題的重疊性質,對每個子問題只計算一次,而後將其計算結果保存在一個表格中,當再次須要計算已經計算過的子問題時,只是在表格中簡單地查看一下 結果,從而得到較高的效率。優化
總結而言,一個問題是該用遞推、貪心、搜索仍是動態規劃,徹底是由這個問題自己階段間狀態的轉移方式決定的:ui
每一個階段只有一個狀態->遞推;spa
每一個階段的最優狀態都是由上一個階段的最優狀態獲得的->貪心;.net
每一個階段的最優狀態是由以前全部階段的狀態的組合獲得的->搜索;code
每一個階段的最優狀態能夠從以前某個階段的某個或某些狀態直接獲得而無論以前這個狀態是如何獲得的->動態規劃。
求解動態規劃的關鍵,是對問題狀態的定義和狀態轉移方程的定義。動態規劃中遞推式的求解方法不是動態規劃的本質。動態規劃算法分如下4個必須步驟:
劃分階段:按照問題的時間或空間特徵,把問題分爲若干個階段。注意這若干個階段必定要是有序的或者是可排序的(即無後向性),不然問題就沒法用動態規劃求解。
描述最優解的結構,即狀態的定義:將問題發展到各個階段時所處於的各類客觀狀況用不一樣的狀態表示出來。固然,狀態的選擇要知足無後效性。 無後向性即每一個階段的最優狀態能夠從以前某個階段的某個或某些狀態直接獲得可是不用管以前的狀態是如何獲得的。
遞歸定義最優解的值,即狀態轉移方程的定義。
按自底向上的方式計算最優解的值 。
由計算出的結果構造一個最優解。 //此步若是隻要求計算最優解的值時,可省略。
這裏咱們以LIS問題作一個實例講解,給定一個數列,長度爲N,求這個數列的最長上升(遞增)子數列(LIS)的長度。以1 7 2 8 3 4爲例,這個數列的最長遞增子數列是 1 2 3 4,長度爲4;次長的長度爲3, 包括 1 7 8; 1 2 3 等。首先咱們對這個問題進行階段劃分,能夠看出求某個數列的最長遞增子數列這個問題確定能夠劃分爲多個階段進行處理,即具有了最優子結構與重疊子問題。下面咱們定義解的結構,即狀態,給定一個數列,長度爲N,設$F_k$爲:以數列中第k項結尾的最長遞增子序列的長度。求解$F_1 \dots F_N$中的最大值。那麼狀態轉移方程,即DP方程也就是:
$$F_1 = 1$$
$$F_k = max(F_i + 1 | A_k > A_i, i \in (1 \dots k-1))(k>1)$$
#include<iostream> using namespace std; /* **該程序針對上一個程序的修改就是: 有一種特殊狀況:求狀態d[i]時,發現有2個不一樣的最長子序列 而上面的方法只是保存了其中的一種。 在此程序中爲了在一個數組result[i][]中保存多條最長子序列, 我採用了間隔符的方式,PAUSE表示一條子序列的結束,用來間隔 下一條子序列在數組爲設置EBD表示後面沒有數據了 */ #define MAXSIZE 100 #define END -100 #define PAUSE -99 int len[MAXSIZE]; int result[MAXSIZE][MAXSIZE]; void LIS(int* a,int n) { int i,j,z,k; int length=1; result[0][0]=a[0]; result[0][1]=END; for(i=0;i<n;i++) { len[i]=1; for(j=0;j<i;j++) if(a[j]<=a[i]) //當a[j]<a[i]時,符合狀態知足的條件,此時分2中狀況 { //第一種狀況:len[j]+1大於len[i],此時len[i]對應的狀態解集 //應當被覆蓋,原來的沒用了。 if(len[j]+1>len[i]) { len[i]=len[j]+1; for(k=0,z=0;result[j][z]!=END;z++,k++) { if(result[j][z]!=PAUSE) // result[i][k]=result[j][z]; else //PAUSE表示一條記錄已結束,下一條開始 { //此時須要的處理是加入本身a[i],同時也設置標記符PAUSE result[i][k++]=a[i]; result[i][k]=PAUSE; } } result[i][k++]=a[i]; result[i][k]=END; } //第二種狀況,len[j]+1等於len[i],此時說明,len[i]之前對應的 //result[i][]是有用的,不該該覆蓋掉(保留的緣由是由於咱們題目要求是顯示全部的子序列結果) else if(len[j]+1==len[i]) { for(k=0;result[i][k]!=END;k++) ; result[i][k++]=PAUSE; for(z=0;result[j][z]!=END;z++,k++) { if(result[j][z]!=PAUSE) // result[i][k]=result[j][z]; else //PAUSE表示一條記錄已結束,下一條開始 { result[i][k++]=a[i]; result[i][k]=PAUSE; } } result[i][k++]=a[i]; result[i][k]=END; } } if(length<len[i]) length=len[i]; } cout<<"長度爲:"<<length<<endl; for(z=0;z<n;z++) if(len[z]==length) { for(i=0;result[z][i]!=END;i++) { if(result[z][i]==PAUSE) { i++; cout<<endl; } cout<<result[z][i]<<" "; } cout<<endl; } } int main() { int a[]={1,3,2,9,11}; LIS(a,5); return 0; }
一個字符串的子序列,是指從該字符串中 去掉任意多個字符 後剩下的字符在 不改變順序的狀況下 組成的新字符串。 最長公共子序列,是指多個字符串可具備的長度最大的公共的子序列。動態規劃採用二維數組來標識中間計算結果,避免重複的計算來提升效率。
由最長公共子序列問題的最優子結構性質可知,要找出$X= \lbrace x1, x2, …, xm\rbrace$和$Y=\lbrace y1, y2, …, yn\rbrace$的最長公共子序列,可按如下方式遞歸地進行:當xm=yn時,找出Xm-1和Yn-1的最長公共子序列,而後在其尾部加上xm(=yn)便可得X和Y的一個最長公共子序列。當xm≠yn時,必須解兩個子問題,即找出Xm-1和Y的一個最長公共子序列及X和Yn-1的一個最長公共子序列。這兩個公共子序列中較長者即爲X和Y的一個最長公共子序列。
由此遞歸結構容易看到最長公共子序列問題具備子問題重疊性質。例如,在計算X和Y的最長公共子序列時,可能要計算出X和Yn-1及Xm-1和Y的最長公共子序列。而這兩個子問題都包含一個公共子問題,即計算Xm-1和Yn-1的最長公共子序列。
與矩陣連乘積最優計算次序問題相似,咱們來創建子問題的最優值的遞歸關係。用c[i,j]記錄序列Xi和Yj的最長公共子序列的長度。其中Xi=<x1, x2, …, xi>,Yj=<y1, y2, …, yj>。當i=0或j=0時,空序列是Xi和Yj的最長公共子序列,故c[i,j]=0。其餘狀況下,由定理可創建遞歸關係以下:
//求最長的公共子序列 #include<iostream> #include<string> using namespace std; #define MAXSIZE 300 string a; string b; int c[MAXSIZE][MAXSIZE]; int d[MAXSIZE][MAXSIZE]; //動態規劃的方式求解最長公共子序列問題 void LCS(int m,int n) { int i,j; for(i=1;i<=m;i++) for(j=1;j<=n;j++) { if(a[i-1]==b[j-1]) { c[i][j]=c[i-1][j-1]+1; d[i][j]=0; } else if(c[i-1][j]>c[i][j-1]) { c[i][j]=c[i-1][j]; d[i][j]=1; } else { c[i][j]=c[i][j-1]; d[i][j]=-1; } } } void display_LCS(int m,int n) //採用回溯方式 { int i=m,j=n; int len=c[m][n]; char s[MAXSIZE]; s[len--]='\0'; while(i>0&&j>0) { if(d[i][j]==0) { s[len]=a[i-1]; len--; i--; j--; } else if(d[i][j]==1) i--; else j--; } cout<<s; } int main() { a="ABCBDAB"; b="BDCABA"; int m,n; m=a.size(); n=b.size(); LCS(m,n); cout<<c[m][n]<<endl; display_LCS(m,n); return 0; }
在序列X={A,B,C,B,D,A,B}和 Y={B,D,C,A,B,A}上,由LCS_LENGTH計算出的表c和b。第i行和第j列中的方塊包含了c[i,j]的值以及指向b[i,j]的箭頭。在c[7,6]的項4,表的右下角爲X和Y的一個LCS的 長度。對於i,j>0,項c[i,j]僅依賴因而否有xi=yi,及項c[i-1,j]和c[i,j-1]的值,這幾個項都在c[i,j]以前計 算。爲了重構一個LCS的元素,從右下角開始跟蹤b[i,j]的箭頭便可,這條路徑標示爲陰影,這條路徑上的每個「↖」對應於一個使xi=yi爲一個 LCS的成員的項(高亮標示)。