演算法導論-動態規劃-鋼條切割

QingLiXueShi發表於2015-03-16

動態規劃通常用於解決最優化問題,在這類問題中,通過做出一組選擇來達到最優解。在做出每個選擇的同時,通常會生成與原問題形式相同的子問題。當多於一個選擇子集都生成相同的子問題時,動態規劃技術通常就會很有效,其關鍵技術就是對每個這樣的子問題都儲存其解,當其重複出現時即可避免重複求解。

 

鋼條切割問題

Serling公司購買長鋼條,將其切割為短鋼條出售。切割工序本身沒有成本支出。公司管理層希望知道最佳的切割方案。假定我們知道Serling公司出售一段長為i英寸的鋼條的價格為pi(i=1,2,…,單位為美元)。鋼條的長度均為整英寸。圖15-1給出了一個價格表的樣例。

鋼條切割問題是這樣的:給定一段長度為n英寸的鋼條和一個價格表pi(i=1,2,…n),求切割鋼條方案,使得銷售收益rn最大。注意,如果長度為n英寸的鋼條的價格pn足夠大,最優解可能就是完全不需要切割。

 

一、問題分析

長度為n英寸的鋼條共有2n-1種不同的切割方案,因為在距離鋼條左端i(i=1,2,…n)英寸處,總是可以選擇切割或不切割。

如果一個最優解將鋼條切割為k段(對某個),那麼最優切割方案,將鋼條切割為長度分別為i1,i2...ik的小段得到的最大收益為

對於其中,pn對應不切割,對於每個i=1,2,…,n-1,首先將鋼條切割為長度為i和n-i的兩段,接著求解這兩段的最優切割收益ri和rn-i(每種方案的最優收益為兩段的最優收益之和)。當完成首次切割後,我們將兩段鋼條看成兩個獨立的鋼條切割問題例項。通過組合兩個相關子問題的最優解,並在所有可能的兩段切割方案中選取組合收益最大者,構成原問題的最優解。

 

鋼條切割問題還存在一種相似的但更為簡單的遞迴求解方法:將鋼條從左邊切割下長度為i的一段,只對右邊剩下的長度為n-i的一段繼續進行切割,對左邊的一段則不再進行切割。這樣得到的公式為:。這樣原問題的最優解只包含一個相關子問題(右端剩餘部分)的解,而不是兩個。

 

二、演算法實現

1、樸素遞迴演算法

自頂向下遞迴實現參考程式碼為:

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個葉結點,每個葉結點對應一種可能的切割方案。

 

2、動態規劃演算法

樸素遞迴演算法之所以效率很低,是因為它反覆求解相同的子問題。因此,動態規劃方法仔細安排求解順序,對每個子問題只求解一次,並將結果儲存下來。如果隨後再次需要此子問題的解,只需查詢儲存的結果,而不必重新計算。因此,動態規劃方法是付出額外的記憶體空間來節省計算空間。

動態規劃有兩種等價的實現方法。

(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 }

一個測試案例為:

相關文章