高效算法之動態規劃(第15章)

有人說:越炫耀什麼,越缺少什麼。但我卻以爲:越缺少什麼,越覺得別人炫耀什麼。 ——李宮俊《李宮俊的詩》

0. 前言

參考圖書《算法導論》
  動態規劃通常用來解決最優化問題,在這類問題中,我們通常做出一組選擇來表達最優解。在做出這個選擇的同時,通常會生成與原問題形式相同的子問題。當多於一個選擇子集都生成相同的子問題時,動態規劃技術通常很有效,其關鍵技術就是對每一個這樣的子問題都保存其解,當其重複出現的時候即可避免重複求解。這種思想可以將指數時間的算法轉換爲多項式時間的算法。
  動態規劃(dynamic programming)是運籌學的一個分支,是求解決策過程(decision process)最優化的數學方法。動態規劃(dynamic programming)中的programming指的是一種表格法,不是編寫計算機程序。
  

1. 動態規劃解析

採用動態規劃求解的問題的一般要具有3個性質:

(1) 最優化原理:如果問題的最優解所包含的子問題的解也是最優的,就稱該問題具有最優子結構,即滿足最優化原理。
(2) 無後效性:即某階段狀態一旦確定,就不受這個狀態以後決策的影響。也就是說,某狀態以後的過程不會影響以前的狀態,只與當前狀態有關。
(3)有重疊子問題:即子問題之間是不獨立的,一個子問題在下一階段決策中可能被多次使用到。(該性質並不是動態規劃適用的必要條件,但是如果沒有這條性質,動態規劃算法同其他算法相比就不具備優勢)。

動態規劃求解基本步驟:

  1. 刻畫一個最優解的結構特徵。
  2. 遞歸的定義最優解的值。
  3. 計算最優解的值,通常採用自底向上的方法。
  4. 利用計算出的信息構造一個最優解。

2. 動態規劃的應用

2.1 鋼條切割

問題陳述:給定一個長度爲n 英寸的鋼條和一個價格表pii=1,2,3,4....,n ,求切割鋼條的方案,使得銷售收入最大。

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

問題分析:從題目中我們可以得出給定的長度爲n 的鋼條有2n1 種不同的切割方案,因爲在距離鋼條左端i(i=1,2,3..,n1) 英寸處,我們總是可以選擇切割或者不切割。對於上述的價格表樣例,我們可以觀察所有最優收益值ri(i=1,2,3,4...10) 以及對應的最優切割方案:

r1=1 , 切割方案1=1(無切割)
r2=5 , 切割方案2=2(無切割)
r3=8 , 切割方案3=3(無切割)
r4=10 , 切割方案4=2+2
r5=13 , 切割方案5=2+3
r6=17 , 切割方案6=6(無切割)
r7=18 , 切割方案7=6+1或者7=2+2+3
r8=22 , 切割方案8=2+6
r9=25 , 切割方案9=3+6
r10=30 , 切割方案10=10(無切割)
  更一般的對於rn(n>=1) , 我們可以用更短的鋼條的最優切割收益來描述它:

rn=max(pn,r1+rn1,r2+rn2,r3+rn3....rn2+r2,rn1+r1)

注意,爲了求解規模爲n的原問題,我們求解形式完全一樣(最優解結構特徵刻畫),完成首次切割之後我們將兩段鋼條看作兩個獨立的鋼條切割問題,通過組合兩個相關子問題的最優解(最優子結構),並且在所有可能的兩段切割方案中選取組合收益最大者,構成原問題的最優解。
除了上述求解方式還有一種遞歸求解的方式公式如下:
rn=max(pi+rn1)(1in)

算法設計:
自頂向下的普通遞歸算法設計:

SteelCut(p[],n){
    //初次判斷
    if(n==0){
        return 0;
    }
    int q=-1;
    //不做切割
    if(n<=p.length){
        q=p[n];
    }
    //遞歸求解
    for(i=1 to n){
        q=max(q,p[i]+SteelCut(p[],n-i));
    }
    return q;
}

使用動態規劃的算法設計:

//算法1:帶備忘錄的自頂向下法
MemorizedSteelCut(p[],n){
    int r[] = new int[n];
    for(i=0 to n){
        r[i]=-1;
    }
    return MemorizedSteelCutAux(p[],n,r);
}
MemorizedSteelCutAux(p[],n,r){
    if(r[n]>=0){
        return r[n];
    }
    if(n==0){
        q=0;
    }
    else{
        q=-1;
        for(i=1 to n){
            q=max(q,p[i]+MemorizedSteelCutAux(p[],n-i,r));      
        }
    }
    r[n]=q;
    return q;
}

//算法2:自底向上法
BottomUpSteelCut(p[],n){
    int r[] = new int[n];
    r[0]=0;
    for(j=1 to n){
        q=-1;
        for(i=1 to j){
            q=max(q,p[i]+r[j-i]);
        }
        r[j]=q;
    }
}

Java實現:

package lbz.ch15.dp.ins1;
/** 
 * @author LbZhang
 * @version 創建時間:2016年3月4日 下午2:20:33 
 * @description 鋼條切割問題
 */
public class SteelCutting {

    public static void main(String[] args) {
        System.out.println("DP 在鋼條切割問題中的應用 ");
        int price[] = {1,5,8,9,10,17,17,20,24,30};
        int n = 10;
        //自頂向下的遞歸實現
        int result = 0;
        result = TopToBottomRecursion(price,n);
        System.out.println("常規的思路:"+result);

        //使用動態規劃來實現  備忘錄
        result=MemorizedSteelCut(price,n);
        System.out.println("備忘錄法:"+result);

        //使用動態規劃的自底向上非遞歸的實現
        result=BottomToTopSteelCut(price,n);
        System.out.println("自底向上非遞歸方法:"+result);


    }
    private static int BottomToTopSteelCut(int[] price, int n) {
        int r[] = new int[n+1];
        r[0]=0;//動態表的開頭
        for(int j=1;j<=n;j++){
            int q=-1;
            for(int i=1;i<=j;i++){
                q=maxOfTwo(q,price[i-1]+r[j-i]);
            }
            r[j]=q;
        }

        return r[n];
    }
    /**
     * 備忘錄方法
     * @param price
     * @param n
     * @return
     */
    private static int MemorizedSteelCut(int[] price, int n) {
        int r[] = new int[n+1];
        for(int i=0;i<=n;i++){
            r[i]=-1;
        }
        return MemorizedSteelCutAux(price,n,r);
    }
    /**
     * 輔助過程的備忘錄核心算法
     * @param price
     * @param n
     * @param r
     * @return
     */
    private static int MemorizedSteelCutAux(int[] price, int n, int[] r) {
        if(r[n]>=0){
            return r[n];
        }
        int q=0;

        if(n==0){
            q=0;
        }else{
            q=-1;
            for(int i=1;i<=n;i++){
                //price[i-1] 應爲price的下標是從0開始,
                q=maxOfTwo(q,price[i-1]+MemorizedSteelCutAux(price,n-i,r));     
            }
        }

        r[n]=q;
        return q;
    }
    /**
     * //自頂向下的遞歸實現 常規思路
     * @param price                                 ``````````````````````````````````````````````````````````````````
     * @param n
     * @return
     */

    private static int TopToBottomRecursion(int[] price, int n) {
        if(n==0) return 0;
        int q = -1;
        if(n<=price.length){
            q=price[n-1];
        }
        for(int i=1;i<n;i++){
            q=maxOfTwo(q,price[i-1]+TopToBottomRecursion(price,n-i));
        }

        return q;


    }
    private static int maxOfTwo(int x, int y) {
        return x>y?x:y;//三目運算符的使用
    }



}

重構解-對源程序進行修改

private static int BottomToTopSteelCut(int[] price, int n) {
        int r[] = new int[n+1];
        int s[] = new int[n+1];

        r[0]=0;//動態表的開頭
        for(int j=1;j<=n;j++){
            int q=-1;
            for(int i=1;i<=j;i++){
                //q=maxOfTwo(q,price[i-1]+r[j-i]);
                if(q<price[i-1]+r[j-i]){
                    q=price[i-1]+r[j-i];
                    s[j]=i;
                }
            }
            r[j]=q;
        }
        System.out.println();
        for(int temp=0;temp<=n;temp++){
            System.out.print(s[temp]+"|-"+temp+"-|");
        }
        System.out.println();

        //正確的組合輸出
        printToFormal(s);


        return r[n];
    }
    private static void printToFormal(int[] s) {
        int len=s.length-1;
        int temp=s[len];

        System.out.print("鋼條切割的組合方式: "+temp+" ");
        while(temp!=len){
            len=len-temp;
            temp=s[len];
            System.out.print("+ "+temp+" ");
        }
        System.out.println();

    }

2.2 斐波那契數列

下面直接給出斐波那契數列的Java實現的O(n) 的動態規劃算法實現。

package lbz.ch15.dp.ins1;

/**
 * @author LbZhang
 * @version 創建時間:2016年3月7日 下午9:42:15
 * @description 類說明
 */
public class MemoryAndTable {
    static int MAX = 20;
    static int[] lookUp = new int[MAX];

    public static int fibMemory(int n) {
        if (lookUp[n] == 0) {
            if (n <= 1) {
                lookUp[n] = n;
            } else {
                lookUp[n] = fibMemory(n - 1) + fibMemory(n - 2);
            }
        }
        return lookUp[n];
    }

    // //打表(自下而上)
    public static int fibTable(int n) {
        int[] f = new int[n + 1];
        int i;
        f[0] = 0;
        f[1] = 1;
        for (i = 2; i <= n; i++) {
            f[i] = f[i - 1] + f[i - 2];
        }
        return f[n];
    }

    public static void main(String[] args) {
        int n = 5;
        System.out.println(fibMemory(9));
        System.out.println();
        // int res=0;
        // res=fibTable(9);
        System.out.println(fibTable(9));
    }
}

注意:在動態規劃中,子問題解決方案被存儲在一個表中,以便這些不必重新計算。 因此,如果這個問題是沒有共同的(重疊)子問題, 動態規劃是沒有用的。例如,二分查找不具有共同的子問題。

發佈了69 篇原創文章 · 獲贊 34 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章