【算法導論】第15章動態規划


 



 

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、具體實現代碼如下:

View Code
 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]);//返回總的標量乘法數。

  具體實現代碼如下:

View Code
 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 具體實現

 

View Code
 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)具體實現

View Code
 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編程


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM