鋼條切割問題帶你徹底理解動態規划


動態規划 (Dynamic Programming)

什么是動態規划?


動態規划算法通常基於一個遞推公式及一個或多個初始狀態。當前子問題的解將由上一個子問題的解推出。
動態規划和分治法相似,都是通過分解,求解,並組合子問題來求解原問題。分治法將問題划分成相互獨立互不相交的子問題,遞歸求解子問題,再將它們的解組合起來,求出原問題的解。與之相反,動態規划應用於子問題重疊的情況,即不同的子問題具有公共的子子問題。在這種情況下,分治算法會做出許多不必要的工作,它會反復的求解那些公共子子問題。而動態規划算法對每個子子問題只求解一次,將結果保存到表格(數組)中,從而無需每次求解一個子子問題都要重新計算。

動態規划之鋼條切割問題

 

假定我們知道某公司出售一段長度為i英寸的鋼條的價格為p[i](i=1,2,3….)鋼條長度為整英寸如圖給出價格表的描述(任意長度的鋼條價格都有)

現在先給一段長度為n的鋼條,問怎么切割,獲得的收益最大 rn?
考慮n=4的時候,有以下8種切割方式

假如一個最優解把n段切成了k段(1<=k<=n),那么最優切割方案:
   i及下標表示第i段的長度,n為鋼條的總長度。

最大收益:
    p及下標表示第i段的收益,r為鋼條的總收益。

 

接下來對這個問題進行求解,我們先用普通的遞歸方法求解:

 

我們從鋼條的左邊切下長度為i的一段,只對右邊剩下長度為n-i的一段繼續進行切割,對左邊的不再切割。

這樣,當第一段長度為n的時候,收益為p[n],剩余長度為0,收益為0(這也是遞歸的基本問題),對應的總收益為p[n]。

當第一段長度為i的時候,收益為p[i],剩余長度為n-i,對應的總收益為p[i]加上剩余的n-i段再進行當第一段長度為i的時候,收益為p[i],剩余長度為n-i-i,....直到剩余長度為0,收益為0。
所以遞歸方程式為:

    pi就是就是p[i],可以看出每次都要進行從1到n的遍歷。

代碼實現 - 自頂向下遞歸實現

 1 #include <iostream>
 2 int UpDown(int n, int * p)//參數n是長度,參數p是價格表
 3 {
 4     if (n == 0) return 0;//遞歸的基本問題
 5     int tempMaxPrice = 0;
 6     for (int i = 1; i < n + 1; i++)
 7     {
 8         int maxPrice = p[i] + UpDown(n - i, p);
 9         if (maxPrice > tempMaxPrice)
10         {
11             tempMaxPrice = maxPrice;
12         }
13     }
14     return tempMaxPrice;
15 }
16 int main()
17 {
18     int p[11]{ 0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30 };//索引代表 鋼條的長度,值代表價格
19     std::cout << UpDown(4,p) <<std::endl;
20 }

 

動態規划的方法進行求解


上面的方法之所以效率很低,是因為它反復求解相同的子問題。比如求r[9]和r[8]的時候都求解了r[7],就是說r[7]被求解了兩次。因此,動態規划算法安排求解的順序,對每個子問題只求解一次,並將結果保存到數組中。如果隨后再次需要此子問題的解,只需查找保存的結果,不必重新計算。因此動態規划的方法是付出額外的內存空間來節省計算時間。

動態規划有兩種等價的實現方法(我們使用上面的鋼條切割問題為例,實現這兩種方法)

第一種方法是 帶備忘的自頂向下法:
    此方法依然是按照自然的遞歸形式編寫過程,但過程中會保存每個子問題的解(通常保存在一個數組中)。當需要計算一個子問題的解時,過程首先檢查是否已經保存過此解。如果是,則直接返回保存的值,從而節省了計算時間;如果沒有保存過此解,按照正常方式計算這個子問題。我們稱這個遞歸過程是帶備忘的。

代碼實現 - 自頂向下動態規划實現

 1 #include <iostream>
 2 int result[11]{ 0 };
 3 int UpDown(int n, int* p)//求得長度為n的最大收益
 4 {
 5     if (n == 0) return 0;
 6     if (result[n] != 0)//這里直接返回記錄的結果
 7     {
 8         return result[n];
 9     }
10     int tempMaxPrice = 0;
11     for (int i = 1; i < n + 1; i++)
12     {
13         int maxPrice = p[i] + UpDown(n - i, p);
14         if (maxPrice > tempMaxPrice)
15         {
16             tempMaxPrice = maxPrice;
17         }
18     }
19     result[n] = tempMaxPrice;//將計算過的長度為n的鋼條切割的最大收益記錄起來
20     return tempMaxPrice;
21 }
22 int main()
23 {
24     int p[11] = { 0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30 };//索引代表 鋼條的長度,值代表價格
25     std::cout << UpDown(4,p);
26 }

 

第二種方法是 自底向上法(常用的方法):
    首先恰當的定義子問題的規模,使得任何問題的求解都只依賴於更小的子問題的解。因而我們將子問題按照規模排序,按從小到大的順序求解。當求解某個問題的時候,它所依賴的更小的子問題都已經求解完畢,結果已經保存到了數組中。

代碼實現 - 自底向上動態規划實現

 

 1 #include <iostream>
 2 int result[11]{ 0 };
 3 int BottomUp(int n, int* p)
 4 {
 5     for (int i = 1; i < n + 1; i++)
 6     {
 7         int tempMaxPrice = 0;
 8         for (int j = 1; j <= i; j++)//下面取得 鋼條長度為i的時候的最大收益
 9         {
10             int maxPrice = p[j] + result[i - j];
11             if (maxPrice > tempMaxPrice)
12             {
13                 tempMaxPrice = maxPrice;
14             }
15         }
16         result[i] = tempMaxPrice;
17     }
18     return result[n];
19 }
20 int main()
21 {
22 
23     int p[11] = { 0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30 };//索引代表 鋼條的長度,值代表價格
24     std::cout << BottomUp(4,p);
25 }

 

 

 


可以看出自頂向下的動態規划求解和普通的遞歸求解差不多,不過動態規划遞歸調用時帶了備忘錄,記錄了已經解決的問題,所以對於上文提到的r[7],我們只求解了一次。
自底向上的動態規划也用了備忘錄,不過它只是迭代求解,並沒有進行遞歸,所以這也是我們常用方法。

 

 

以上有什么不足的地方和應該改進的地方,歡迎各路大神批評指正,筆者一定虛心接受。謝謝!

 

 


免責聲明!

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



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