1. 程式人生 > >動態規劃演算法——鋼條切割問題

動態規劃演算法——鋼條切割問題

動態規劃是通過組合子問題的解來求解原問題。與分治方法不同的是,動態規劃應用於子問題重疊的情況,即不同的子問題具有公共的子子問題。在這種情況下,分治策略會重複的計算那些公共子問題。而動態規劃是對每個子子問題只求解一次,將其儲存在一個表格中,從而避免重複計算這些問題。
動態規劃通常用於求解最優化問題(optimization problem)。這類問題擁有多個解,我們希望從中計算出最優解。當然,有些問題可能會不止一個最優解,此時,我們只需按照需求或計算一個最優解,或計算出所有的最優解。
通常有4個步驟設計一個動態規劃演算法。

1. 刻畫一個最優解的結構特徵;
2. 遞迴的定義最優解的值;
3. 計算最優解的值,通常有兩種方法,自底向上和自頂向下。
4. 利用計算出的資訊構造一個最優解。

鋼條切割的例子

給定一個長度為n英寸的鋼條和一個價格表pi(i=1,2,...n),求切割鋼條的方案,使得銷售收益 rn 最大。
假設切割長度與價格表如下所示

長度i 1 2 3 4 5 6 7 8 9 10
價格pi 1 5 8 9 10 17 17 20 24 30

因為在鋼條左端i(i=1,2,...n1)處可以選擇切割和不切割兩種方式,所以長度為n的鋼條共有2(n1)中切割方案。當n=4時,即有一個長度為4 的鋼條,怎樣切割才能獲得最大收益。可以有8中方案,如下。

  • 4 = 4 , 收益r
    =9
  • 4 = 1+3,收益為r=9
  • 4 = 2+2,收益為r=10
  • 4 = 3+1 ,收益為r=9
  • 4 = 1+1+2, 收益為r=7
  • 4 = 1+2+1, 收益為r=7
  • 4 = 2+1+1,收益為r=7
  • 4=1+1+1+1,收益為r=4

很明顯,當4=2+2時,可以得到最高的收益。那麼,我們怎樣用形式化的語言來描述它呢?

  1. 我們先切割第一段,我們可以在第1個位置切割,即4=1+3,也可以在第2個位置切割,即4=2+2等等。
  2. 就得到1+3,2+2,3+1這些種情況。同時,還要考慮鋼條不切割的情況。
  3. 計算最大收益。因為我們的目的是求怎樣切割(或者乾脆不切割)會得到最大收益。那麼,最大收益r=
    max(r(4),r(3)+r(1),r(2)+(2),r(1)+r(3)
  4. 依次切割,直到得到最大收益。

假設一個最優解是要將鋼條切割成k段,那麼最優解的切割方案為:

n=i1+i2+i3+...+ik
將鋼條切割長度分別為i1,i2,...ik長度的鋼條,得到的最大收益為:
rn=p(i1)+p(i2)+...p(ik)
更一般的,對於rn(n>=1),我們可以用更短的鋼條的最優切割來描述它。
rn=max(pn,r1+rn1,r2+rn2,...ri+rni)
第一個引數pn為不切割的方案。其他n1個引數對應n1種方案:對每個i=1,2,3..n1,首先將鋼條的分為長度為ini的兩段鋼條,分別求出這兩段鋼條的最優解rirni(每種方案的最優收益為兩段的最優收益之和)。
為了求解規模為n的原問題,我們先求解形式完全一樣,但規模更小的子問題。即當完成首次切割後,我們將兩段鋼條看成兩個獨立的鋼條切割問題例項。我們通過組合相關子問題的最優解,並在所有可能的兩段切割方案中選取收益最大的,構成原問題的最優解。鋼條問題滿足最優子結構

最優子結構:問題的最優解由相關子問題的最優解組合而成,而這些子問題都可以獨立求解。

使用動態規劃解決鋼條切割的問題

雖然我們可以用較少較容易理解的遞迴程式碼解出此問題,然而,遞迴會重複計算相同的子問題,導致程式的執行時間以指數級的速度增長,即時間複雜度為O(2n)

動態規劃的思想:使用遞迴方法之所以效率這麼低,是因為它會重複計算相同的子問題。因此,動態規劃方法仔細安排求解順序,對每個子問題只求解一次,並將其結果儲存下來。如果再次需要子問題的解,只需查詢儲存的結果,而不必重新計算。因此,動態規劃方法時典型的時空權衡(time-memory trade-off)的例子。

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

帶備忘錄的自頂向下方法(top-down with memorization)

此方法仍然按照自然的遞迴形式編寫過程,但過程中會儲存每個子問題的解(通常是儲存在陣列或者散列表中),當需要一個子問題的解時,會先判斷是否儲存過此解。如果是,則直接返回儲存的值,從而節省了計算時間。否則,按照常規方式進行計算。

程式碼如下


public static int memorizedCutRod(int[] p, int n){
        int result = 0;
        //儲存已計算過的子問題的解的陣列
        int res[] = new int[n+1];
        for (int i = 0; i < res.length; i++) {
            res[i] = -1;
        }
        result = memorizedCutRodAux(p, n, res);
        return result;
    }

    public static int memorizedCutRodAux(int[] p, int n, int[] r){
        //如果已經計算過該子問題的解,直接返回
        if(r[n]>=0){
            return r[n];
        }

        int q = -1;
        if(n==0){
             q = 0;
        } else{
            //r(n)=max(p[i]+r(n-i))
            //p[i]表示切割成長度為i的鋼條的收益
            //r(n-i)剩餘鋼條的最大收益值
             for(int i = 1;i<=n;i++){
                 q = Math.max(q, p[i]+memorizedCutRodAux(p, n-i, r));
             }
        }
        r[n] = q;
        return q;
    }
自底向上的方法(bottom-up method)

這種方法一般需要恰當定義子問題的規模的概念,使得任何子問題的求解都只依賴於更小的子問題的求解。因而,我們可以將子問題按照規模排序,按照有小到大的順序進行求解。當求解某個子問題時,它所依賴的那些更小的子問題都已經求解完畢,結果已經儲存。每個子問題只需要求解一次,當我們求解它時,它的所有前提子問題都已經求解完成。

public static int bottomUpCutRod(int[] p, int n){
        //儲存子問題的結果,res[n]就是我們所需的最優解
        int[] res = new int[n+1];
        //依次求出規模為i = 1...n的子問題
        for(int i=1; i<=n; i++){
            int q = -1;
            for(int j=1;j<=i;j++){
                q = Math.max(q, p[j]+res[i-j]);
            }
            res[i] = q;
        }

        return res[n];

    }

現在我們只是求出來收益的最大值,並沒有求得解本身,即給出切割後每段鋼條的長度。我們可以擴充套件動態規劃演算法,使之對每個子問題不僅儲存最優收益值,還儲存對應的切割方案。

程式碼如下


public static void extendedBottomUpCutRod(int[] p, int n){
        int[] res = new int[n+1];
        //用來儲存最優解的切割的鋼條的長度
        int[] solve = new int[n+1];

        res[0] = 0;
        for (int i = 1; i <= n; i++) {
            int q = -1;
            for(int j = 1; j<=i;j++){
                if(q<p[j]+res[i-j]){
                    q = p[j]+res[i-j];
                    solve[i] = j;
                }
            }
            res[i] = q;
        }

        print(res[n], solve, n);
    }

    public static void print(int maxValue, int[] solve, int n) {
        System.out.println(maxValue);
        while(n>0){
            System.out.print(solve[n]+", ");
            n = n - solve[n];
        }
    }

測試程式碼


public static void main(String[] args) {
        int[] p = {0,1,5,8,9,10,17,17,20,24,30};
        int n = 9;
        int result = memorizedCutRod(p, n);
        System.out.println(result);
        int result2 = bottomUpCutRod(p, n);
        System.out.println(result);

        extendedBottomUpCutRod(p, n);
    }