1、問題引入
和分治法一樣,動態規划是通過組合子問題的解而解決整個問題的。分治法是指將問題划分成一些獨立的子問題,遞歸求各個子問題,然后合並子問題的解而得到原問題的解。而動態規划適用於子問題不獨立的情況,也就是各個子問題包含公共的“子子問題”,在這種情況下,分治法將不便於求解,而動態規划算法將對每個“子子問題”只求一次解,將其結果保存在一張表中,從而避免每次遇到各個子問題時重新計算答案。
動態規划通常應用於最優化問題,此類問題可能有多種可行解,每個解有一個值,而我們希望找出一個具有最優值的解,稱這樣的解為該問題的“一個”最優解(而不是“確定的”最優解),因為可能存在多個取最優值的解。
動態規划的設計可以分成如下4個步驟:
(1)描述最優解的結構;
(2)遞歸定義最優解的值;
(3)按自底向上的方式計算最優解的值;
(4)由計算結果構造一個最優解。
接下來將利用動態規划方法求解幾個問題:裝配線調度問題、矩陣鏈乘法問題、求最長的公共子序列問題、構造最優二叉查找樹問題。
2、裝配線調度
2.1 問題描述
某汽車工廠有2個裝配線,每個裝配線有n 個裝配站(按順序編號1~n )標記為Si,j,表示第i個裝配線的第j個裝配站,兩個裝配線的對應的裝配站執行相同的功能,但所用的時間可能不同。經過第i條流水線(i=1,2)的第j 個裝配站所花的時間為a[i][j]。從第i條流水線的第j 個裝配站移到第j+1個裝配站的時間可以忽略,而移到另外一個流水線的下一個裝配站則需要一定的時間t[i][j]。汽車進入流水線需要花時間,進入裝配線1需要時間e[1],進入裝配線2需要時間e[2]; 汽車出流水線時需要花時間,出裝配線1需要時間x[1],出裝配線2需要時間x[2] 。汽車的裝配需要按順序經過所有裝配站。
現在已知裝配時間a[i][j] 、轉移時間t[i][j]、進入裝配線的時間e[i]、出裝配線的時間x[i],要求輸出裝配一輛汽車所需要的最短時間,以及經過的裝配站。
2.2 求解過程
(1)最優子結構
對於裝配線問題,推理如下:一條經過裝配線S1,j的最快路徑,必定是經過裝配線1或2上的裝配站j-1.因此通過S1,j的最快路徑只能是以下二者之一:
(a)通過裝配站S1,j-1的最快路徑,然后通過裝配站S1,j;
(b)通過裝配站S2,j-1的最快路徑,從裝配線2移動到裝配線1,然后通過裝配站S1,j。
對於裝配線2,有類似結論。
(2)一個遞歸的解
最終目標是確定通過工廠所有的裝配線的最快時間,記為f*。設f[i][j]為一個汽車從起點到裝配站Si,j的最快可能時間。汽車必然是經過裝配線1或2最終到達裝配站n,然后到達工廠的出口。即:
f*=min(f[1][n]+x[1] , f[2][n]+x[2])
要對f1[n]和f2[n]進行推理可以運用步驟1。
初始化:f[1][1]=e[1]+a[1][1]
f[2][1]=e[2]+a[2][1]
計算f[i][j],其中j=2,3...,n;i=1,2
f[1][j]=min( f[1][j-1]+a[1][j] , f[2][j-1]+t[2][j-1]+a[1][j])
f[2][j]=min( f[2][j-1]+a[2][j] , f[1][j-1]+t[1][j-1]+a[2][j])
為了便於跟蹤最優解的構造過程,定義l[i][j]:i為裝配線的編號,i=1,2 ,j表示裝配站j-1被通過裝配站Si,j的最快路線所使用,j=2,3,...,n;
(3)自底向上計算最優解的值
用算法描述如下:
1 int fastestWay 2 f[1][1]=e[1]+a[1][1]; 3 f[2][1]=e[2]+a[2][1]; 4 for(j=2;j<n;j++) 5 { 6 if(f[1][j-1]+a[1][j]<=f[2][j-1]+t[2][j-1]+a[1][j]) 7 { 8 f[1][j]=f[1][j-1]+a[1][j]; 9 l[1][j]=1; 10 } 11 else 12 { 13 f[1][j]=f[2][j-1]+t[2][j-1]+a[1][j]; 14 l[1][j]=2; 15 } 16 17 if(f[2][j-1]+a[2][j]<=f[1][j-1]+t[1][j-1]+a[2][j]) 18 { 19 f[2][j]=f[2][j-1]+a[2][j]; 20 l[2][j]=2; 21 } 22 else 23 { 24 f[2][j]=f[1][j-1]+t[1][j-1]+a[2][j]; 25 l[2][j]=1; 26 } 27 }
2.3、具體實現代碼如下:

1 #include<stdio.h>
2 #include<stdlib.h>
3 const int n=7; 4 int fastestWay(int l[][n],int f[][n],int a[][n],int t[][n-1],int e[],int x[],int n) 5 { 6 int i,j,ff,ll; 7 f[1][1]=e[1]+a[1][1]; 8 f[2][1]=e[2]+a[2][1]; 9 for(j=2;j<n;j++) 10 { 11 if(f[1][j-1]+a[1][j]<=f[2][j-1]+t[2][j-1]+a[1][j]) 12 { 13 f[1][j]=f[1][j-1]+a[1][j]; 14 l[1][j]=1; 15 } 16 else
17 { 18 f[1][j]=f[2][j-1]+t[2][j-1]+a[1][j]; 19 l[1][j]=2; 20 } 21
22 if(f[2][j-1]+a[2][j]<=f[1][j-1]+t[1][j-1]+a[2][j]) 23 { 24 f[2][j]=f[2][j-1]+a[2][j]; 25 l[2][j]=2; 26 } 27 else
28 { 29 f[2][j]=f[1][j-1]+t[1][j-1]+a[2][j]; 30 l[2][j]=1; 31 } 32 } 33 if(f[1][n-1]+x[1]<=f[2][n-1]+x[2]) 34 { 35 ff=f[1][n-1]+x[1]; 36 l[1][1]=1;//利用l[1][1]保存出站的最后一個裝配站
37 } 38 else
39 { 40 ff=f[2][n-1]+x[2]; 41 l[1][1]=2; 42 } 43 return (ff); 44 } 45
46 void main() 47 { 48 int i,j,k; 49 int a[3][7],t[3][6];//a[i][j]記錄經過裝配線i的裝配站j所用的時間,t[i][j]記錄由裝配線i的裝配站Si,j移動到另一條裝配線所需要的時間
50 int e[3]={0,2,4}; 51 int x[3]={0,3,2}; 52 int l[3][7],f[3][7];//l[i][j]用於記錄通過裝配站Si,j的最快路徑中經過的前一站所在的裝配線
53 int b[2][6]={{7,9,3,4,8,4},{8,5,6,4,5,7}}; 54 int c[2][5]={{2,3,1,3,4},{2,1,2,2,1}}; 55 for(i=0;i<2;i++) 56 for(j=0;j<6;j++) 57 a[i+1][j+1]=b[i][j]; 58 for(i=0;i<2;i++) 59 for(j=0;j<5;j++) 60 t[i+1][j+1]=c[i][j]; 61 k=fastestWay(l,f,a,t,e,x,n); 62 printf("汽車裝配的最少時間為:%d\n",k); 63 int cc[n+1];//用於將l[i][j]中的結果正向輸出
64 cc[n]=l[1][1];//將最后經過的裝配站所在的裝配線放入cc[n]
65 for(i=6;i>=2;i--) 66 cc[i]=l[cc[i+1]][i]; 67 printf("汽車裝配經過的裝配線和裝配站情況如下:\n"); 68 for(i=2;i<=n;i++) 69 printf("line %d , station %d \n",cc[i],i-1); 70 }
3、矩陣鏈乘法
3.1 問題描述
矩陣鏈乘法問題可以描述如下:給定n個矩陣構成一個鏈(A1,A2,A3,... ,An),其中i=1,2,... ,n,矩陣Ai的維數為pi-1*pi,對乘積A1A2...An以一種最小化標量乘法次數的方式進行加全部括號。
3.2 求解步驟
(1)最優加全部括號的結構
動態規划的第一步是尋找最優子結構,假設AiAi+1...Aj的一個最優加全部括號把乘積在Ak與Ak+1之間分開,則對AiAi+1...Aj最優加全部括號的“前綴”子鏈AiAi+1...Ak的加全部括號必須是AiAi+1...Ak的一個最優加全部括號。
(2)一個遞歸解
根據子問題的最優解來遞歸定義一個最優解的代價。對於矩陣鏈乘法問題,子問題即確定AiAi+1...Aj的加全部括號的最小代價問題,此處1<=i<=j<=n。設m[i][j]為計算矩陣Ai...j所需的標量乘法運算次數的最小值;對整個問題,計算A1...n的最小代價就是m[1][n]。
m[i][j]=0。i=j時
m[i][j]=min{ m[i][k]+m[k+1][j]+pi-1pkpj} 在i!=j時。
定義s[i][j]為這樣的一個k值:在該處分裂乘積AiAi+1...Aj后可得一個最優加全部括號。亦即s[i][j]等於使得m[i][j]取最優解的k值。
(3)計算最優代價
具體算法如下:
1 for(i=1;i<n;i++) 2 m[i][i]=0; 3
4 for(l=2;l<=c;l++)//確定步長,即i與j之間的距離,l=2時表示j-i等於1
5 { 6 for(i=1;i<=c-l+1;i++)//確定起始點
7 { 8 j=i+l-1;//確定終止點
9 m[i][j]=max; 10 for(k=i;k<j;k++)//選取k值,確定起始點和終止點之間的最好k值
11 { 12 q=m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j]; 13 if(q<m[i][j]) 14 { 15 m[i][j]=q; 16 s[i][j]=k; 17 } 18 } 19 } 20 } 21 return(m[1][n-1]);//返回總的標量乘法數。
具體實現代碼如下:

1 #include<stdio.h>
2 #include<stdlib.h>
3 #define n 7
4 #define max 20000
5 int matrixChainOrder(int p[],int s[][n]); 6 void printOptimalParens(int s[][n],int i,int j); 7 void main() 8 { 9 int k,s[n][n]; 10 int p[7]={30,35,15,5,10,20,25}; 11 k=matrixChainOrder(p,s); 12 printf("%d個矩陣相乘所需的標量乘法的最小值為:%d\n",n-1,k); 13 printf("最終的最優全括號形式為:\n"); 14 printOptimalParens(s,1,6); 15 } 16 void printOptimalParens(int s[][n],int i,int j) 17 { 18 if(i==j) 19 printf("A%d",i); 20 else
21 { 22 printf("("); 23 printOptimalParens(s,i,s[i][j]); 24 printOptimalParens(s,s[i][j]+1,j); 25 printf(")"); 26 } 27 } 28 int matrixChainOrder(int p[],int s[][n]) 29 { 30 int i,l,j,k,c=n-1; 31 int q,m[n][n]; 32 for(i=0;i<n;i++) 33 for(j=0;j<n;j++) 34 m[i][j]=0; 35 for(i=1;i<n;i++) 36 m[i][i]=0; 37
38 for(l=2;l<=c;l++)//確定步長,即i與j之間的距離,l=2時表示j-i等於1
39 { 40 for(i=1;i<=c-l+1;i++)//確定起始點
41 { 42 j=i+l-1;//確定終止點
43 m[i][j]=max; 44 for(k=i;k<j;k++)//選取k值,確定起始點和終止點之間的最好k值
45 { 46 q=m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j]; 47 if(q<m[i][j]) 48 { 49 m[i][j]=q; 50 s[i][j]=k; 51 } 52 } 53 } 54 } 55 return(m[1][n-1]);//返回總的標量乘法數。
56 }
4、最長公共子序列
4.1 問題描述
給定兩個序列x和y,稱z是x和y的公共子序列,如果z既是x的子序列,又是y的子序列;最長的公共子序列稱作最長公共子序列LCS(longest common subsequence)。
4.1 求解步驟
(1)LCS的最優子結構
設zk是xm和yn的一個LCS,則,如果x和y的最后一個元素相同,則z中去掉最后一個元素之后zk-1仍為xm-1和yn-1的LCS
如果xm!=yn,若zk!=xm,則z是xm-1和y的一個LCS,若zk!=yn,則z是xm和yn-1的LCS。
(2)一個遞歸解
設c[i][j]為序列xi和yj的一個LCS的長度,則有:
c[i][j]=0 i=0或j=0
c[i][j]=c[i-1][j-1]+1 xi=yj且i,j>0
c[i][j]=max(c[i][j-1] , c[i-1][j]) xi!=yj且i,j>0
(3)計算LCS的長度
具體算法:
1 lcsLength(x,y) 2 m=length(x); 3 n=length(y); 4 for i=1 to m 5 c[i][0]=0; 6 for j=0 to n 7 c[0][j]=0; 8 for i=1 to m 9 for j=1 to n 10 if(x[i]==y[j]) 11 c[i][j]=c[i-1][j-1]+1; 12 else//求二者中的較大值
13 c[i][j]=(c[i-1][j]>=c[i][j-1])?c[i-1][j]:c[i][j-1] 14 return c
4.2 具體實現

1 #include<stdio.h> 2 #include<string.h> 3 #include<malloc.h> 4 void lcsLength(int **p,char x[],char y[],int m,int n) 5 { 6 int i,j; 7 //printf("%d %d",m,n); 8 for(i=1;i<=m;i++) 9 *(*(p+i))=0; 10 for(j=0;j<=n;j++) 11 *(*p+j)=0; 12 for(i=1;i<=m;i++) 13 for(j=1;j<=n;j++) 14 { 15 if(x[i-1]==y[j-1]) 16 *(*(p+i)+j)=*(*(p+i-1)+j-1)+1; 17 else 18 *(*(p+i)+j)=((*(*(p+i-1)+j)>=*(*(p+i)+j-1))? (*(*(p+i-1)+j)):(*(*(p+i)+j-1))); 19 } 20 } 21 void main() 22 { 23 int i,j,k=0; 24 char x[]={"ABCBDAB"}; 25 char y[]={"BDCABA"}; 26 int m=strlen(x);//x序列的長度 27 int n=strlen(y);//y序列的長度 28 int **c=(int **)malloc(sizeof(int)*(m+1));//建立動態數組 29 for(i=0;i<m+1;i++) 30 c[i]=(int*)malloc(sizeof(int)*(n+1)); 31 lcsLength(c,x,y,m,n); 32 printf("最長公共子序列的長度為:\n%d\n",c[m][n]); 33 char *p=(char *)malloc(sizeof(char)*(c[m][n])); 34 printf("其中的一個最長公共子序列為:\n"); 35 i=m; 36 j=n; 37 while(c[i][j]>0)//將公共子序列中的值放入數組p[k]中 38 { 39 if(x[i-1]==y[j-1]) 40 { 41 p[k++]=x[i-1]; 42 i--; 43 j--; 44 } 45 else 46 { 47 if(c[i][j]==c[i-1][j]) 48 i--; 49 else 50 j--; 51 } 52 } 53 for(i=k-1;i>=0;i--) 54 printf("%c ",p[i]); 55 printf("\n"); 56 }
5、最優二叉查找樹
5.1 問題描述
假設正在設計一個程序,用於將文章從英文翻譯為法語,對於出現在文章內的每一個英文單詞,需要查看與它等價的法語。執行這些搜索操作的一種方式是建立一棵二叉查找樹,,,因為要為文章中的每個單詞搜索這棵樹,古故希望搜索所花費的總時間盡可能的小,因此我們希望文章中出現頻繁的單詞唄放置在距離根部較近的地方,而且文章中可能會有些單詞沒有法語的翻譯,這些單詞可能根本就不會出現在二叉查找樹中。
n個關鍵字,對於每個關鍵字ki,一次搜索ki的概率為pi,,,,樹中還存在n+1個虛擬的關鍵字di,一尺搜索di的概率為qi,假設n=5個的關鍵字的集合上的二叉查找樹的概率如下:
,現在要求根據上表構造一棵二叉查找樹,使得二叉查找樹的期望搜索代價最低。
5.2 求解步驟
(1)分析給出一棵最優二叉查找樹的結構
(2)一個遞歸解
定義e[i,j]為搜索一棵包含關鍵字ki,,,kj的最優二叉查找樹的期望代價,最終要計算e[1,n]。。。。
當j=i-1時,只有虛擬鍵di-1,期望的搜索代價是e[i,i-1]=qi-1。
當j>=i時,需要從ki,...,kj中選擇一個根kr,然后用關鍵字ki,...,kr-1來構造一棵最優二叉查找樹作為其左子樹,並用關鍵字kr+1,...,kj來構造一棵最優二叉查找樹作為其右子樹。。。注意當一棵樹成為一個節點的子樹時,它的期望搜索代價增加量將為該子樹中所有概率的總和。對於一棵有關鍵字ki,...,kj的子樹,定義概率的總和為:
w[i,j]=pl(l=i到j)的總和+ql(l=i-1到j的總和)
因此,有
e[i,j]=qi-1 j=i-1
e[i,j]=min{e[i,r-1]+e[r+1,j]+w[i,j]} i<=j
另外定義root[i,j]為kr的下標r。
(3)計算一棵最優二叉查找樹的期望搜索代價
具體算法
1 optimalBst(p,q,n) 2 for i=1 to n+1//初始化 3 e[i,i-1]=q[i-1] 4 w[i,i-1]=q[i-1] 5 for l=1 to n //步長 6 for i=1 to n-l+1 7 j=i+l-1 8 e[i,j]=max//無窮大 9 w[i,j]=w[i,j-1]+p[j]+q[j] 10 for r=i to j//比較 11 t=e[i,r-1]+e[r+1,j]+w[i,j] 12 if t<e[i,j] 13 e[i,j]=t 14 root[i,j]=r 15 return e and root
(4)具體實現

1 #include<stdio.h> 2 #include<malloc.h> 3 #define max 9999 4 #define n 5 5 6 double optimalBst(double p[],double q[],int root[][n+1]) 7 { 8 int i,j,l,r; 9 double t; 10 double w[n+2][n+1]; 11 double e[n+2][n+1]; 12 for(i=1;i<=n+1;i++) 13 { 14 e[i][i-1]=q[i-1]; 15 w[i][i-1]=q[i-1]; 16 } 17 for(l=1;l<=n;l++) 18 { 19 for(i=1;i<=n-l+1;i++) 20 { 21 j=i+l-1; 22 e[i][j]=max; 23 w[i][j]=w[i][j-1]+p[j]+q[j]; 24 for(r=i;r<=j;r++) 25 { 26 t=e[i][r-1]+e[r+1][j]+w[i][j]; 27 if(t<e[i][j]) 28 { 29 e[i][j]=t; 30 root[i][j]=r; 31 } 32 } 33 } 34 } 35 return(e[1][n]); 36 } 37 void printBst(int root[][n+1],int i,int j) 38 { 39 int k; 40 if(i<=j) 41 { 42 printf("%d " ,root[i][j]); 43 k=root[i][j]; 44 printBst(root,i,k-1); 45 printBst(root,k+1,j); 46 } 47 } 48 void main() 49 { 50 int i; 51 double k; 52 int root[n+1][n+1]; 53 double p[n+1]={0,0.15,0.10,0.05,0.10,0.20}; 54 double q[n+1]={0.05,0.10,0.05,0.05,0.05,0.10}; 55 k=optimalBst(p,q,root); 56 printf("最小期望搜索代價為:%f\n",k); 57 printf("最優二叉查找樹的中序遍歷結果為:\n"); 58 printBst(root,1,n); 59 }
附:另外一個用動態規划求左右二叉查找樹的程序:http://www.cnblogs.com/lpshou/archive/2012/04/26/2470914.html
6、有向無環圖的單源最短路徑長度(較簡單)
附:用動態規划求有向無環圖的單源最短路徑:http://www.cnblogs.com/lpshou/archive/2012/04/17/2453370.html
7、0-1背包問題
附:用動態規划求0-1背包問題:http://www.cnblogs.com/lpshou/archive/2012/04/17/2454009.html
8、數塔
附:用動態規划求數塔問題:http://www.cnblogs.com/lpshou/archive/2012/04/17/2453379.html
9、參考資料:
(1):http://blog.csdn.net/xiaoyjy/article/details/2420861
(2):算法導論
(3):c編程