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

數據結構與算法系列源代碼:https://github.com/ThinerZQ/AllAlgorithmInJava
本文源代碼:https://github.com/ThinerZQ/AllAlgorithmInJava/blob/master/src/main/java/com/zq/algorithm/dynamicprogrammin/SteelBar.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)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章