1. 程式人生 > >動態規劃-鋼條切割(java)

動態規劃-鋼條切割(java)

如果程式碼連結失效了,麻煩評論給我。

動態規劃分治法相似,都是通過組合子問題的解來求解原問題。

分治法將問題劃分為不互相交子問題,遞迴的求解子問題,再將他們組合起來,求出原問題的解。

與之相反,動態規劃應用於子問題重疊的情況,即不同的問題具有公共的 子子問題。這種情況下分治法會重複的求解那些公共的子子問題。而動態規劃演算法對每個子子問題只求解一次,將其存放在某一個表格中,無需每次求解一個子子問題時都重新計算,避免了不必要的計算工作,特別是當問題規模比較大的時候,在時間上有顯著的區別

動態規劃用來求解最優化問題。這類問題可以有很多可行的解,每個解都有一個值,我們希望需找具有最優值(最大或最小)的解。當然可能同時存在多個最優解(同時最大,或同時最小),動態規劃只要求找到其中一個就好了。

這裡我們用演算法導論裡面的鋼條切割為例子

切鋼條:假如Serling公司出售一段長度為 i 英寸的鋼條的價格為 pi( i =1,2,3,4…單位問美元)。鋼條的長度為整英寸。

下表是一個價格表

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

假設Serling公司進了一批長度為10的鋼條,那麼怎麼切割才能使利益最大呢,長度為 9 , 8 呢?

對於上述價格表樣例,我們可以觀察出所有最優收益值Ri及對應的最優解方案:

最優值 切割方案
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 = 1 + 6或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 + Rn-1, R2 + Rn-2,…,Rn-1 + R1)

首先將鋼條切割為長度為i和n - i兩段,接著求解這兩段的最優切割收益Ri和Rn - i(每種方案的最優收益為兩段的最優收益之和),由於無法預知哪種方案會獲得最優收益,我們必須考察所有可能的i,選取其中收益最大者。如果直接出售原鋼條會獲得最大收益,我們當然可以選擇不做任何切割。

注意到,為了求解規模為n的原問題,我們先求解形式完全一樣,但是規模更小的子問題。當完成首次切割之後,我們將兩段鋼條看成兩個同等的鋼條切割問題例項,通過組合兩個問題的最優解,並在所有可能的兩段切割方案中選擇組合收益最大值,構成原問題的最優解,我們稱這樣的問題滿足最優子結構性質問題的最優解是由相關子問題的最優解組和而成的,這些子問題可以獨立求解。

分析到這裡,假設現在出售10英寸的鋼條,應該怎麼切割呢?為了方便分析,我們使用鋼條長度=4來分析問題

1、解法:

1.1 遞迴法:


 /**
     * 遞迴方法,時間複雜度為O(2的N次方),因為考察了 2的N-1次方種可能
     * @param p,鋼條的價格陣列,
     * @param n,鋼條的長度,這裡的劃分是以 1 為單位
     * @return 最大收益
     */
    public int cut_rod(int[] p,int n){
        //遞迴出口,n=0,不用切割了。
        if ( n==0){
            System.out.println("呼叫子問題規模:0");
            return 0;
        }
        // q 是最大值,初始值設為為一個負值,
        int q=-1;
        //對於每一次遞迴呼叫,都會求1..n之間的最優質,然後返回給上一層

        for (int i=1;i<=n;i++){
            //當前長度為 n 的切割收益的最大值,是當前的 q .和p[i]+cut_rod(p,n-i)中的最大值,迴圈中時不斷改變q值的,
            System.out.println("呼叫子問題規模:"+n);
            q=max(q,p[i]+cut_rod(p,n-i));
            if (i==n){
                System.out.println("子問題規模為 "+n+" 的最優值 = "+q);
            }

        }
        System.out.println("回到第:"+(n+1)+"層");
        System.out.println();
        return q;
    }
    public int max(int a,int b){
        return a>b?a:b;
    }

1.1.1 分析:

上面程式碼的遞迴中,始終會重複執行太多相同的操作例如cut_rod(p,4)會遞迴呼叫cut_rod(p,3),cut_rod(p,2),cut_rod(p,1),cut_rod(p,0),當呼叫cut_rod(p,3) 的時候,又會遞迴呼叫cut_rod(p,2),…cut_rod(p,1)……..

1.1.2 程式輸出結果:

這裡寫圖片描述

1.1.3 程式遞迴分析

這裡寫圖片描述

………..如此多的重複遞迴是沒有必要的,這也是動態規劃所要處理的問題。

怎麼避免重複呼叫呢??
動態規劃的做法是,將每一次求得的cut_rod(p,i)的最優值儲存在一個表(陣列)裡面,每次需要使用的時候,不用再遞迴呼叫了,直接使用就好了。

1.2 動態規劃——>帶備忘的自頂向下法:

/**
     * 動態規劃方法
     *              帶備忘的自頂向下法
     * @param p,鋼條的價格陣列,
     * @param n,鋼條的長度,這裡的劃分是以 1 為單位
     * @return 最大收益
     */
    public int memoized_cut_rod(int[] p,int n){
        //一個數組,用r[i] 來儲存 鋼條長度為 i 的時候的最優值,初始值賦為 -1.一個負值就行。
        int[] r= new int[n+1];
        for (int i=0;i<r.length;i++){
            r[i]=-1;
        }
        //呼叫遞迴的那個方法,返回長度為 n的最優值。
        return memoized_cut_aux(p,n,r);
    }

    /**
     *
     * @param p,鋼條的價格陣列,
     * @param n,鋼條的長度,這裡的劃分是以 1 為單位
     * @param r 儲存中間值的陣列
     * @return 最大收益
     */
    public int memoized_cut_aux(int[] p,int n,int[] r){
        //遞迴出口,如果r[n] >0,表明,長度為 n 的鋼條的最優值已經存在了。不用遞迴了,直接返回這個最優值,這裡必須是r[n]>=0,因為r[0]是等於0的,
        if (r[n]>=0){
            System.out.println();
            System.out.print("   ------直接返回r[" + n + "] = " + r[n] );

            return r[n];
        }
        //設定零時變數 q 最為最大值
        int q=-1;
        //剛進入遞迴的時候,剛開始一路呼叫下來,必然是從這個口出去。
        if (n==0){

            q=0;
            System.out.print(" 呼叫 n ="+q + "    第一次儲存r[0]的值:" + q);
        }else {
            //遞迴呼叫,求解最大值。
            System.out.println(" 呼叫 n ="+n);
            for (int i=1;i<=n;i++){

                q = max(q,p[i]+memoized_cut_aux(p,n-i,r));
                System.out.print("   開始回溯到n="+n);
                if (i==n){
                    System.out.println();
                }
               // System.out.println();
            }

        }
        System.out.println();
        //將每一次求的長度為 n 的最優值儲存在陣列 r 裡面
        r[n]=q;
        //返回最大值

        if (n==r.length-1){
            System.out.println("程式結束,返回r["+n+"]="+r[n]);
        }
        return q;
    }

1.2.1 分析:

上面使用自頂向下的方法,求解問題,就像深度優先搜尋二叉樹一樣。具體分析見下圖

1.2.2 程式輸出結果:

這裡寫圖片描述

1.2.3 程式遞迴分析:

這裡寫圖片描述

1.3 動態規劃——>自底向上法:

  /**
     * 動態規劃,自底向上求解。
     * @param p,鋼條的價格陣列,
     * @param n,鋼條的長度,這裡的劃分是以 1 為單位
     * @return 最大收益
     */
    public int bottomUpCutRod(int[] p,int n){
        //一個數組,用r[i] 來儲存 鋼條長度為 i 的時候的最優值,初始值賦為 0.
        int[] r= new int[n+1];
        for (int i=0;i<r.length;i++){
            r[i]=0;
        }

        //迴圈,外層依次求解 1....n的最優值
        for (int j=1;j<=n;j++){
            int q=-1;
            //內層,依次在 1 .. j 中求出最大值,
            //例如
            // 當 j =1 的時候,q=max(q,p[1]+r[0]) .求的r[1]的最優值
            // 當 j =2 的時候,q=max(q,p[1]+r[1]),然後再是 q=max(q,p[2]+r[0])  ,求的r[2]的最優值
            //  ... 以此類推
            for (int i=1;i<=j;i++){
                q=max(q,p[i]+r[j-i]);
            }
            //記錄 j 的最優值
            r[j]=q;
        }
        //最終返回 n 的最優值
        return r[n];
    }

1.3.1 分析:

這個方法是動態規劃最佳方法,具體見後面的總結

1.3.2 程式結果:

這裡寫圖片描述

1.3.3 程式分析:

這裡寫圖片描述

自底向上的方法,不必進行遞迴呼叫,而是直接訪問陣列元素r[j-i]來獲得規模為j-i的子問題的解。同時也將規模為j的解存入r[j]。就像上圖一樣,r[0]的解是0,r[1]的解依靠r[1] ,r[2]的解依靠r[0]和r[1]…..r[4]的解依靠r[0]和r[1],r[2],和r[3].

上面只是求出了,鋼條長度為 i 的最優值,那麼怎麼切割呢?下面砸門來看看

1.4 求解切割方案

直接上程式碼,extended_button_up_cut_rod函式和自底向上求解最優值的函式是一樣的,不同點就是加入了一個儲存切割方案的陣列s,每次找到最優值的時候,記錄切割方案。

 /**
     * 求解最優值和組合方案
     * @param p 價格表
     * @param n 鋼條長度
     * @param r 最優值陣列,
     * @param s 切割方案陣列
     */
    public void extended_button_up_cut_rod(int[] p,int n,int[] r,int[] s){


        //迴圈,外層依次求解 1....n的最優值
        for (int j=1;j<=n;j++){
            int q=-1;
            //內層,依次在 1 .. j 中求出最大值,
            //例如
            // 當 j =1 的時候,q=max(q,p[1]+r[0]) .求的r[1]的最優值
            // 當 j =2 的時候,q=max(q,p[1]+r[1]),然後再是 q=max(q,p[2]+r[0])  ,求的r[2]的最優值
            //  ... 以此類推
            for (int i=1;i<=j;i++){
                if (q<p[i]+r[j-i]){
                    q=p[i]+r[j-i];
                    //記錄長度為 j 的鋼條 第一下開始切割的位置 i .
                    s[j]=i;
                }
            }
            //記錄 j 的最優值
            r[j]=q;
        }
    }

    /**
     * 輸出最優值和切割方案的函式
     * @param p 價格表
     * @param n 鋼條長度
     */
    public void print_cut_rod_solution(int[] p,int n){
        //一個數組,用r[i] 來儲存 鋼條長度為 i 的時候的最優值,初始值賦為 0.
        int[] r= new int[n+1];
        for (int i=0;i<r.length;i++){
            r[i]=0;
        }
        int[] s = new int[n+1];
        for (int i=0;i<r.length;i++){
            s[i]=0;
        }
        //呼叫求最優值和方案的函式
        extended_button_up_cut_rod(p,n,r,s);

        System.out.print("n="+n+" 的最優值為:"+r[n]+" , 切割方案為:");
        //當 n>0 的時候,表明還有長度需要切割,哪怕做0切割
        while (n>0){
            //輸出,組合方案
            System.out.print(s[n] + "+");
            //改變 n 的值,n=s[n]表示 已經切割下了s[n]那麼長,剩下的要怎麼切割
            n=n-s[n];
        }
    }

1.4.1 程式結果:

可以參考上面給出的表格中的資料
這裡寫圖片描述

1.4總結:

第一種直接的自頂向下的遞迴方法,沒有考慮子問題重疊問題時間複雜度為指數級問題規模稍微大一點,比如(n=30),時間複雜度就不能忍受了。

第二種自上而下的帶備忘錄遞迴方法,考慮了子問題重疊問題,利用空間來儲存求得的結果,時間複雜度為o(n^2),效果較好。

第三種自下而上的方法,很自然的考慮了子問題重疊問題時間複雜度為o(n^2),沒有頻繁的遞迴呼叫的開銷,這種方法具有更下的係數。更好。和第二種方法空間複雜度一樣都是O(n)