動態規划方法通常用來求解最優化問題。
適合使用動態規划求解最優化問題應具備的兩個要素:
1、最優子結構:如果一個問題的最優解包含子問題的最優解,那么該問題就具有最優子結構。
2、子問題重疊(如果子問題不重疊就可以用遞歸的方法解決了)
具備上述兩個要素的問題之所以用動態規划而不用分治算法是因為分治算法會反復的調用重疊的子問題導致,效率低下,而動態規划使用了運用了空間置換時間的思想,將每一個已解決的子問題保存起來,這樣重復的子問題只需要計算1次,所以時間效率較高。
動態規划算法設計步驟:
1.刻畫一個最優解的結構特征。
2.遞歸定義最優解的值。
3.計算最優解的值,通常采用自底向上的方法。
4.利用計算出的信息構造一個最優解。
其中發掘最優子結構的過程遵循下面的通用模式:
1. 證明問題最優解的第一個組成部分是做出一個選擇。
2. 對於給定問題,在其可能的第一步選擇中,假定已經知道哪種選擇才會得到最優解,但並不關心這種選擇具體是如何得到的,只是假定已經知道了這種選擇。
3. 給定可獲得最優解的選擇后,確定這次選擇會產生哪些子問題,以及如何最好地刻畫子問題空間。
4. 利用 “剪切-粘貼”(cut-and-paste)技術證明:作為構成原問題最優解的組成部分,每個子問題的解就是它本身的最優解(利用反證法)。
動態規划的實現方法:
帶備忘的自頂向下法:此方法仍按自然的遞歸形式編寫過程,但過程會保存每個子問題的解(通常保存在一個數組或散列表中)。當需要一個子問題的解時,過程首先檢查是否已經保存過此解。如果是,則直接返回保存的值,從而提高時間效率。
自底向上法:這種方法一般需要恰當定義子問題“規模”的概念,使得任何子問題的求解都依賴於“更小的”子問題的求解。因而我們可以將子問題按規模排序,按由小至大的順序進行求解。當求解某個子問題時,它所依賴的那些更小的子問題都已經求解完畢,結果已經保存。每個子問題只需要求解一次,當我們求解它(也是第一次遇到它)時,它的所有前提子問題都已求解完成。
下面應用動態規划解決一個問題
serling公司購買長鋼條,將其切割為鍛鋼條出售。切割工序沒有成本。先給出出售一段長度為i的鋼條的價格為p(i),對應關系如下表,求給一段長度為n(n<=10)的鋼條,要切割多少次才能以最高的價格賣出?
長度i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
價格p(i) | 1 | 5 | 8 | 9 | 10 | 17 | 17 | 20 | 24 | 30 |
首先這個問題求解要多少次才能以最高的價格賣出,是一個求最優化的問題,接下來分析下該問題是否具有適用於最優化問題的兩個特征,假設n=5的話,要求解長度為5的鋼條怎么切割才能獲得最優解,首先我們知道將長度為5的鋼條進行切割,第一刀可以選擇0+5、1+4、2+3三種切割方案,然后在每一種切割方案中,將剩下的繼續選擇最優方案(和遞歸的思想一樣,將原問題轉換成更小的子問題),可以列出下列關系式:r(5)=max{p(1)+r(5-1),p(2)+r(5-2),p(3)+r(5-3),p(4)+r(5-4),p(5)+r(0)} 式中r(i)表示總長度為i時的最大收益。可以看出求解r(5)的最優解包含了求解其子問題r(4)、r(3)、r(2)、r(1)、r(0)的最優解,
更為通用的表達式就是r(n)=max{p(1)+r(n-1),p(2)+r(n-2),......p(n-1)+r(1),p(n)+r(0)}可以看出通用的表達式里面的最優解包含了其子問題的最優解,所以該問題符合最優子結構的特征,然后再看有沒有第二個特征,還是以n=5進行分析,下圖顯示了求解子問題的遞歸樹
從遞歸樹中可以看出有大量的求解都是重疊的,所以也滿足動態規划的第二個特征,那么這個問題選擇用動態規划的方法來求解很可能是一個很好的辦法!
經過分析已經得出遞歸最優子結構:r(n)=max{p(1)+r(n-1),p(2)+r(n-2),......p(n-1)+r(1),p(n)+r(0)}
帶備忘的自頂向下法偽代碼:
1 memoized_cut(p,n) 2 let r[0...n]be a new array 3 for i=0 to n 4 r[i]=-1 5 return memoized_cut_digui(p,n,r) 6 7 8 9 memoized_cut_digui(p,n,r) 10 if(r[n]>=0) 11 return r[n] 12 if(0==n) 13 temp=0
14 else 15 temp=-1 16 for i=1 to n 17 if(p[i]+memoized_cut_digui(p,n-i,r)>temp) 18 temp=p[i]+memoized_cut_digui(p,n-i,r) 19 r[n]=temp 20 return temp 21 22
帶備忘的自頂向下法C++程序:
1 #include<iostream> 2 using namespace std; 3 int memoized_cut_digui(int *p,int n,int *r) 4 { 5 int temp; 6 if(r[n]>=0) 7 { 8 return r[n]; 9 } 10 if(n==0) 11 { 12 temp=0; 13 } 14 else temp=-1; 15 for(int i=1;i<=n;i++) 16 { 17 if((p[i]+memoized_cut_digui(p,n-i,r))>temp) 18 { 19 temp=p[i]+memoized_cut_digui(p,n-i,r); 20 } 21 } 22 r[n]=temp; 23 return temp; 24 } 25 int memoized_cut(int *p,int n) 26 { 27 int *r=new int[n]; 28 memset(r,-1,n); //將r數組全部賦值為-1 29 return memoized_cut_digui(p,n,r); 30 } 31 int main() 32 { 33 int p[11]={0,1,5,8,9,10,17,17,20,24,30}; 34 int n; 35 cin>>n; 36 cout<<memoized_cut(p,n); 37 return 0; 38 }
自底向上法偽代碼:
1 memoized_cut(p,n) 2 let r[0...n]be a new array 3 r[0]=0 4 for i=1 to n 5 temp=-1 6 for j=1 to i 7 if(p[j]+r[i-j]>temp) 8 temp=p[j]+r[i-j] 9 r[i]=temp 10 return r[n]
自底向上法C++代碼
1 #include<iostream> 2 using namespace std; 3 int memoized_cut(int *p,int n) 4 { 5 int *r=new int[n+1]; 6 r[0]=0; 7 //從r[1]逐次求解一直到r[n] 8 for(int i=1;i<=n;i++) 9 { 10 int temp=-1; 11 //求解每一個r[i]的時候都需要將它及它之前的每一段先切第一刀的可能性都遍歷一遍,然后求這次遍歷中得到的最大值為這個i下的最優解 12 for(int j=1;j<=i;j++) 13 { 14 if(p[j]+r[i-j]>temp) 15 { 16 temp=p[j]+r[i-j]; 17 } 18 19 } 20 r[i]=temp; 21 } 22 return r[n]; 23 } 24 25 int main() 26 { 27 int p[11]={0,1,5,8,9,10,17,17,20,24,30}; 28 int n; 29 cin>>n; 30 cout<<memoized_cut(p,n); 31 return 0; 32 }
重構解
前面只求出了最優的收益值,並沒有返回解的本身(沒有給出最優的情況下,應該分成每個子段的長度值),為此我們可以在動態規划保存最優解的同時保存切割方案,然后對最優方案進行輸出。
偽代碼如下:
1 memoized_cut(p,n) 2 let r[0...n]be a new array 3 r[0]=0 4 for i=1 to n 5 temp=-1 6 for j=1 to i 7 if(p[j]+r[i-j]>temp) 8 temp=p[j]+r[i-j] 9 s[i]=j//保存最優解 10 r[i]=temp 11 return r[n] and s 12 13 14 15 16 print_zuiyoujie(s,n) 17 while n>0 18 cout s[n] 19 n=n-s[n]
C++程序如下:
1 #include<iostream> 2 using namespace std; 3 int memoized_cut(int *p,int n,int *s) 4 { 5 int *r=new int[n+1]; 6 r[0]=0; 7 //從r[1]逐次求解一直到r[n] 8 for(int i=1;i<=n;i++) 9 { 10 int temp=-1; 11 //求解每一個r[i]的時候都需要將它及它之前的每一段先切第一刀的可能性都遍歷一遍,然后求這次遍歷中得到的最大值為這個i下的最優解 12 for(int j=1;j<=i;j++) 13 { 14 if(p[j]+r[i-j]>temp) 15 { 16 temp=p[j]+r[i-j]; 17 *(s+i)=j; 18 } 19 20 } 21 r[i]=temp; 22 } 23 return r[n]; 24 } 25 26 int main() 27 { 28 int p[11]={0,1,5,8,9,10,17,17,20,24,30}; 29 int *s=NULL; 30 s=new int [11]; 31 int n; 32 cin>>n; 33 cout<<"最大的收益為"<<memoized_cut(p,n,s)<<endl; 34 cout<<"最佳切割方案是:"<<endl; 35 while(n) 36 { 37 cout<<s[n]<<endl; 38 n=n-s[n]; 39 } 40 return 0; 41 }