《算法導論》中動態規劃求解鋼條切割問題

動態規劃算法概述ios

  動態規劃(dynamic programming1是一種與分治方法很像的方法,都是經過組合子問題的解來求解原問題。不一樣之處在於,動態規劃用於子問題重疊的狀況,好比咱們學過的斐波那契數列。在斐波那契數列的求解問題中,咱們常常要對一個公共子問題進行屢次求解,而動態規劃算法,則對每一個子問題只求解一次,將其解保存在一個表格中,從而避免了大量的冗餘計算量。算法

  動態規劃算法經常使用於尋找最優解問題(optimization problem)。而其規劃大概可分爲四步:編程

  1.刻畫一個最優解的結構特徵。函數

  2.遞歸的定義最優解的值。學習

  3.計算最優解的值。spa

  4.利用計算出的信息構造一個最優解2code

  咱們將以《算法導論》中的一個習例做爲展現的對象,講解動態規劃算法的應用方法。對象

鋼條切割問題blog

現有某公司,購買長鋼條以切割成短鋼條出售。若不計切割成本,請求出如何切割以使公司利益最大。該公司短鋼條售價以下:遞歸

長度:1 2 3 4 5 6 7 8 9 10

價格:1 5 8 9 10 17 20 24 30

  

  現假設一段鋼條的長度爲n咱們能夠試求當 n = 4時,咱們能得到的最大收益。此時,咱們對於第一次分割有5種選擇(0,1,2,3,4),以此類推,對於n = 4的狀況,咱們一共有8種情形:(4),(1,3),(2,2),(3,1),(1,1,2),(1,2,1),(2,1,1),(1,1,1,1)。易算得,最高價值爲(2,2)狀況下取得,最高收益爲10.

  那麼,咱們如何用函數來描述這一過程呢?首先,咱們能夠假設第一次切割所得的第一部分鋼條長度爲x (屬於[0,n])。則咱們如今有了兩根鋼條,一根長爲x,另外一根長爲 n - x。那麼長爲n的鋼條所得利益的最優解就來自於長爲x n - x兩段鋼條最優解的和。如此劃分,便可將問題逐步化簡爲一個個子問題,以R來表示公司所得利益,P來表示鋼條單價,則有Rn的函數:             

Rn = maxPx + Rn - x))

  

普通遞歸方法實現

  能夠看出上述公式是一個很明顯的遞歸函數,咱們很容易就能夠獲得下列代碼

 

 1 #include<iostream>
 2 #include<vector>
 3 #include<algorithm>
 4 #define null -1
 5 using namespace std;
 6  
 7 int cut_rod(int n, vector<int> p) {
 8 if (n == 0)  return 0;
 9 int q = null;                                   
10 for (int i = 1; i <= n; i++) {
11 q = max(q , p[i-1] + cut_rod(n - i, p) );
12 }
13 return q;
14 }
15  
16 int main() {
17 cout << "輸入產品各段數所對應的價格(從小到大)" << endl;
18 int n = 0;
19 vector<int> p;
20 while (cin >> n  && n != null) {                            //輸入-1表示中止
21 p.push_back(n);
22 n = 0;
23 }
24 vector<int> results(p.size() + 1, null);                    //在result中建立n + 1個元素([0,n]共n+1個),並統一賦值爲null
25 cout << "請輸入所需切割鋼材長度" << endl;
26 cin >> n;
27 cout << cut_rod(n, p);
28 //cout << memo_cut_rod(n, p, results);
29 //cout << bot_cut_rod(n, p, results);
30 return 0;
31 }

 

咱們已在斐波那契數列的學習中證實了,這種算法的缺點是很明顯的,隨着遞歸的深刻,其計算量會爆炸性的增加。易得其時間複雜度 T = 2N

  那麼咱們要怎麼利用動態規劃的方法來進行簡便運算呢?方法有兩種:一種稱之爲帶備忘的自頂向下法(top-down with memoization3),另外一種則是自底向上法(bottom-up method)。

帶備忘的自頂向下法

  此方法與正常的遞歸方法並沒有太大區別,但在過程當中,每個子問題的解都會被保存下來,在每次求解以前都會驗證是否已經對該子問題進行了求解,如果,則直接返回保存的值;不是,再進行正常運算。據此理論,易得代碼:

 1 int memo_cut_rod(int n, vector<int> p, vector<int> results) {
 2 if (results[n] > 0) return results[n];
 3 if (n == 0) return 0;
 4 int q = null;
 5 for (int i = 1; i <= n; i++) {
 6 q = max(q, p[i - 1] + memo_cut_rod(n - i, p,results));
 7 }
 8 return q;
 9 }
10  
11  
12 int main() {
13 cout << "輸入產品各段數所對應的價格(從小到大)" << endl;
14 int n = 0;
15 vector<int> p;
16 while (cin >> n  && n != null) {                            //輸入-1表示中止
17 p.push_back(n);
18 n = 0;
19 }
20 vector<int> results(p.size() + 1, null);                    //在result中建立n + 1個元素([0,n]共n+1個),並統一賦值爲null
21 cout << "請輸入所需切割鋼材長度" << endl;
22 cin >> n;
23 //cout << cut_rod(n, p);
24 cout << memo_cut_rod(n, p, results);
25 return 0;
26

 

自底向上法

  自底向上法採用了與正常遞歸類似的順序,但免除了從頂到下的過程以及冗餘的計算,直接從最小問題算起,最後構成最優解。其代碼以下:

int bot_cut_rod(int n,vector<int> p,vector<int> results) {
results[0] = 0;
for (int j = 1; j <= n; ++j) {
int q = null;
for (int i = 1; i <= j; ++i) {
q = max(q, p[i -1] + results[j - i]);
}
results[j] = q;
}
return results[n];
}

 

總結

  自底向上法與帶備忘的自頂向下法具備相同的漸進運行時間,二者的時間複雜度都爲 T = n2。相比以前的2n強了太多。而使用動態規劃算法的重中之重,是找好問題劃分的方法,將問題一步一步化簡到最小,把大問題化簡成一個個小問題,小問題每每比大問題好解的多,最後再由小問題推導出大問題的答案,就算是大功告成了。    

 

 

 

 

 

 

註釋:

1.此處的programming是指一種表格法,而非編程

2.當咱們僅僅須要一個最優解的值的時候,咱們每每能夠省略掉第4步。

3.此處並不是拼寫錯誤,確實爲memoization,而非memorization。前者源自memo,爲備忘之意。

 

            參考文獻:

《算法導論》

相關文章
相關標籤/搜索