動態規劃一般用於解決最優化問題,在這類問題中,經過作出一組選擇來達到最優解。在作出每一個選擇的同時,一般會生成與原問題形式相同的子問題。當多於一個選擇子集都生成相同的子問題時,動態規劃技術一般就會頗有效,其關鍵技術就是對每一個這樣的子問題都保存其解,當其重複出現時便可避免重複求解。ios
鋼條切割問題算法
Serling公司購買長鋼條,將其切割爲短鋼條出售。切割工序自己沒有成本支出。公司管理層但願知道最佳的切割方案。假定咱們知道Serling公司出售一段長爲i英寸的鋼條的價格爲pi(i=1,2,…,單位爲美圓)。鋼條的長度均爲整英寸。圖15-1給出了一個價格表的樣例。數組
鋼條切割問題是這樣的:給定一段長度爲n英寸的鋼條和一個價格表pi(i=1,2,…n),求切割鋼條方案,使得銷售收益rn最大。注意,若是長度爲n英寸的鋼條的價格pn足夠大,最優解可能就是徹底不須要切割。函數
1、問題分析測試
長度爲n英寸的鋼條共有2n-1種不一樣的切割方案,由於在距離鋼條左端i(i=1,2,…n)英寸處,老是能夠選擇切割或不切割。優化
若是一個最優解將鋼條切割爲k段(對某個),那麼最優切割方案,將鋼條切割爲長度分別爲i1,i2...ik的小段獲得的最大收益爲。spa
對於,。其中,pn對應不切割,對於每一個i=1,2,…,n-1,首先將鋼條切割爲長度爲i和n-i的兩段,接着求解這兩段的最優切割收益ri和rn-i(每種方案的最優收益爲兩段的最優收益之和)。當完成首次切割後,咱們將兩段鋼條當作兩個獨立的鋼條切割問題實例。經過組合兩個相關子問題的最優解,並在全部可能的兩段切割方案中選取組合收益最大者,構成原問題的最優解。3d
鋼條切割問題還存在一種類似的但更爲簡單的遞歸求解方法:將鋼條從左邊切割下長度爲i的一段,只對右邊剩下的長度爲n-i的一段繼續進行切割,對左邊的一段則再也不進行切割。這樣獲得的公式爲:。這樣原問題的最優解只包含一個相關子問題(右端剩餘部分)的解,而不是兩個。code
2、算法實現blog
一、樸素遞歸算法
自頂向下遞歸實現參考代碼爲:
int CutRod(const int *p, int n) { if (n == 0) { return 0; } int q = -1; for (int i = 1; i <= n; ++i) { int tmp = p[i] + CutRod(p, n - i); if (q < tmp) { q = tmp; } } return q; }
分析:自頂向下遞歸實現的CutRod效率不好,緣由在於CutRod反覆地用相同的參數值對自身進行遞歸調用,即它反覆求解相同的子問題。它的運行時間爲。對於長度爲n的鋼條CutRod考察了全部2n-1種可能的切割方案。遞歸調用樹共有2n-1個葉結點,每一個葉結點對應一種可能的切割方案。
二、動態規劃算法
樸素遞歸算法之因此效率很低,是由於它反覆求解相同的子問題。所以,動態規劃方法仔細安排求解順序,對每一個子問題只求解一次,並將結果保存下來。若是隨後再次須要此子問題的解,只需查找保存的結果,而沒必要從新計算。所以,動態規劃方法是付出額外的內存空間來節省計算空間。
動態規劃有兩種等價的實現方法。
(1)帶備忘的自頂向下法
此方法仍按天然的遞歸形式編寫過程,但過程會保存每一個子問題的解(一般保存在一個數組或散列表中)。當須要一個子問題的解時,過程首先檢查是否已經保存過此解。若是是,則直接返回保存的值,從而節省了計算時間;不然,按一般方式計算這個子問題。
(2)自底向上法
這種方法通常須要恰當定義子問題「規模」的概念,使得任何子問題的求解都只依賴於「更小的」子問題的求解。所以,咱們能夠將子問題按照規模順序,由小至大順序進行求解。當求解某個子問題時,它所依賴的那些更小的子問題都已求解完畢,結果已經保存。每一個子問題只需求解一次,當咱們求解它時,它的全部前提子問題都已求解完成。
說明:兩種方法獲得的算法具備相同的漸進運行時間,僅有的差別是在某些特殊狀況下,自頂向下方法並未真正遞歸地考察全部可能的子問題。因爲沒有頻繁的遞歸函數調用的開銷,自底向上方法的時間複雜度函數一般具備更小的係數。
下面給出動態規劃-帶備忘的自頂向下過程參考代碼:
1 int MemoizedCutRodAux(const int *p, int n, int *r) 2 { 3 if (r[n] >= 0) 4 { 5 return r[n]; //首先檢查所需的值是否存在 6 } 7 8 int q = -1; 9 if (n == 0) 10 { 11 q = 0; 12 } 13 else 14 { 15 for (int i = 1; i <= n; ++i) 16 { 17 int tmp = p[i] + MemoizedCutRodAux(p, n - i, r); 18 if (q < tmp) 19 { 20 q = tmp; 21 } 22 } 23 } 24 r[n] = q; 25 26 return q; 27 } 28 29 int MemoizedCutRod(const int *p, int n) 30 { 31 int *r = new int[n + 1]; 32 for (int i = 0; i <= n; ++i) 33 { 34 r[i] = -1; 35 } 36 37 return MemoizedCutRodAux(p, n, r); 38 }
下面給出動態規劃-自底向上過程參考代碼:
int BottomUpCutRod(const int *p, int n) { int *r = new int[n + 1]; r[0] = 0; for (int i = 1; i <= n; ++i) { int q = -1; for (int j = 1; j <= i; ++j) { int tmp = p[j] + r[i - j]; q = q > tmp ? q : tmp; } r[i] = q; } return r[n]; }
說明:自頂向下與自底向上算法具備相同的漸進運行時間。
最後給出重構解參考代碼:
1 #include <iostream> 2 using namespace std; 3 4 void ExtendedBUCutRod(const int *p, int n, int *r, int *s) 5 { 6 r[0] = 0; 7 for (int i = 1; i <= n; ++i) 8 { 9 int q = -1; 10 for (int j = 1; j <= i; ++j) 11 { 12 int tmp = p[j - 1] + r[i - j]; 13 if (q < tmp) 14 { 15 q = tmp; 16 s[i] = j; 17 } 18 } 19 r[i] = q; 20 } 21 } 22 23 //r[]保存長度爲i的鋼條最大收益,s[]保存最優解對應的第一段鋼條的切割長度 24 void PrintBUCutRod(const int *p, int n, int *r, int *s) 25 { 26 ExtendedBUCutRod(p, n, r, s); 27 cout << "長度爲" << n << "的鋼條最大收益爲:" << r[n] << endl; 28 29 cout << "最優方案的鋼條長度分別爲:"; 30 while (n > 0) 31 { 32 cout << s[n] << " "; 33 n = n - s[n]; 34 } 35 cout << endl; 36 } 37 38 //Test 39 int main() 40 { 41 int n; 42 while (cin >> n) 43 { 44 int *p = new int[n]; 45 for (int i = 0; i < n; ++i) 46 { 47 cin >> p[i]; 48 } 49 int *r = new int[n + 1]; 50 int *s = new int[n + 1]; 51 52 PrintBUCutRod(p, n, r, s); 53 } 54 55 return 0; 56 }
一個測試案例爲: