Description:
The rod-cutting problem is the following. Given a rod of length n inches and a table of prices pi for i D 1,2,…,n, determine the maximum revenue rn obtainable by cutting up the rod and selling the pieces. Note that if the price pn for a rod of length n is large enough, an optimal solution may require no cutting at all.
以下給出的樣例:
Analysis:
長度為n的鋼條可以有2n-1中切割方案,所以當n很大時用暴力求解的方法是行不通的。假設將該鋼條切割成k(k的取值為[1,n])段,切割順序為從鋼條的左端開始,那么最有切割方案是:
n = len1+len2+len3+…+lenk
將鋼條切割為長度是len1,len2,…,lenk的小段,得到最大收益
rn = p len1+p len2+p len3+…+p lenk
根據樣例給出的價格表,可以得到最大收益值ri(i=1,2,…,10)及對應的切割方案。
i |
最大收益 |
切割方案 |
1 |
1 |
無切割 |
2 |
5 |
無切割 |
3 |
8 |
無切割 |
4 |
10 |
4 = 2 + 2 |
5 |
13 |
5 = 2 + 3 |
6 |
17 |
無切割 |
7 |
18 |
7 = 1 + 6 或7 = 2 + 2 + 3 |
8 |
22 |
8 = 2 + 6 |
9 |
25 |
9 = 3 + 6 |
10 |
30 |
無切割
|
由上表可以得出:長度為n的鋼條其最大收益值和長度為k和n-k的兩條鋼條的最大收益值相關。
我們可以假設長度為n的鋼條的最大收益值:
rn = max(pn,r1+rn-1,r2+rn-2,…,rn-1+r1),pn為不切割時的價格。
這樣可以通過組合兩個相關子問題的最優解,並在所有可能的兩段切割方案中選取組合收益最大者,構成原問題的最優解。
另一種思路:將鋼條從左邊切割長度為i的一段,只對右邊剩下的長度為n-i的一段繼續進行切割(遞歸求解),對左邊的一段則不再進行分割。
rn = max(pi + rn-i) (i取值范圍[1,n])
在這個公式中,原問題的最優解只包含一個相關子問題(右端剩余部分)的解。
自頂向下的遞歸算法實現
1 //自頂向下遞歸實現,prof[]是樣例給出的價格表 2 int cutRod(int prof[], int n) 3 { 4 if (n == 0) 5 return 0; 6 else 7 { 8 int profit = 0; 9 for (int i = 1; i <= n; i++) 10 profit = max(profit, prof[i] + cutRod(prof, n - i)); 11 return profit; 12 } 13 14 }
根據代碼可以分析得出:
隨着n的增大,程序的運行時間會成倍的增加。原因是,cutRod反復地用相同的參數值對自身進行遞歸調用,即它反復求解相同的子問題。
當 n = 4時,遞歸調用樹如下:
cutRod的運行時間為n的指數函數,T(n)= 2n
使用動態規划方法求解鋼條切割的最優化問題
1、帶備忘的自頂向下法
備忘就是一張存儲每次計算最佳收益的表,這樣就可以避免“自頂向下遞歸算法”中出現的重復計算重疊子問題的情況。
int memoizedCutRodAux(int pro[], int r[], int n) { if (r[n] > 0) return r[n]; else { int profit = 0; for (int i = 1; i <= n; i++) profit = max(profit, pro[i] + memoizedCutRodAux(pro, r, n - i)); r[n] = profit; return profit; } }
2、自底向上法
int bottomUpCutRod(int pro[], int r[], int n) { for (int i = 1; i <= n; i++) { int profit = 0; for (int j = 1; j <= i; j++) profit = max(profit, pro[j] + r[i - j]); r[i] = profit; } return r[n]; }
現在把思路一中rn = max(pn,r1+rn-1,r2+rn-2,…,rn-1+r1)的算法給出
int bottomUpCutRodTwo(int pro[], int r[], int n) { for (int i = 1; i <= n; i++) { int profit = pro[i]; for (int j = 1; j <= i / 2; j++) profit = max(profit, r[j] + r[i - j]); r[i] = profit; } return r[n]; }
對第二個for循環中j <= i / 2;解釋一下,從等式rn = max(pn,r1+rn-1,r2+rn-2,…,rn-1+r1),可以看出,r1+rn-1和rn-1+r1其實是一樣的。所以沒有必要循環到i,截至到i/2即可。
鋼條切割問題的子問題圖
可以看出該圖是一個有向圖,每個頂點唯一地對應一個子問題,若是求子問題x的最優解時需要直接用到其子問題y的最優解,那么在子問題圖中就會有一條從子問題x的頂點到子問題y的頂點的有向邊。子問題G=(V,E)的規模可以確定動態規划的運行時間,由於每個子問題只求解一次,因此算法的運行時間等於每個子問題求解時間之和。通常,一個子問題的求解時間與子問題圖中對應頂點的出度成正比,而子問題的數目等於子問題圖中頂點數。因此,通常情況下,動態規划的運行時間與頂點和邊的數量呈線性關系。
總結:結合動態規划的算法設計步驟來說,鋼條切割問題也是遵守其標准的。
第一步先確定最優化解的結構特征:最優切割方案是由第一次切割后得到的兩段鋼條的最優切割方案組成的,或者是第一次切割后,其右端剩余部分的最優切割方案組成的。
第二步遞歸定義最優解的值,由上面的分析我們可以得到rn = max(pn,r1+rn-1,r2+rn-2,…,rn-1+r1)和rn = max(pi + rn-i) 兩個最優解的公式,其滿足求得原問題最優解的條件。
第三步根據得到的求最優解公式,計算出結果。我們用到了兩種方法:帶備忘的自頂向下遞歸法和自底向上法(非遞歸)。
第四步構造出最優解。