一步一步看清動態規劃----揹包問題(java解)

———————————更新——————————–

動態規劃有兩個很重要的概念,無後效性狀態,這裏參考兩篇文章,講的不錯,尤其是第二篇:
什麼是無後效性?
動態規劃

———————————原文——————————–

題目背景

話說有一哥們去森林裏玩發現了一堆寶石,他數了數,一共有n個。 但他身上能裝寶石的就只有一個揹包,揹包的容量爲C。這哥們把n個寶石排成一排並編上號: 0,1,2,…,n-1。第i個寶石對應的體積和價值分別爲V[i]和W[i] 。排好後這哥們開始思考: 揹包總共也就只能裝下體積爲C的東西,那我要裝下哪些寶石才能讓我獲得最大的利益呢?

OK,如果是你,你會怎麼做?你斬釘截鐵的說:動態規劃啊!恭喜你,答對了。 那麼讓我們來看看,動態規劃中最最最重要的兩個概念: 狀態和狀態轉移方程在這個問題中分別是什麼。

我們要怎樣去定義狀態呢?這個狀態總不能是憑空想象或是從天上掉下來的吧。 爲了方便說明,讓我們先實例化上面的問題。一般遇到n,你就果斷地給n賦予一個很小的數, 比如n=3。然後設揹包容量C=10,三個寶石的體積爲5,4,3,對應的價值爲20,10,12。 大家都知道正解應該是把體積爲5和3的寶石裝到揹包裏, 此時對應的價值是20+12=32。接下來,我們把第三個寶石拿走, 同時揹包容量減去第三個寶石的體積(因爲它是裝入揹包的寶石之一), 於是問題的各參數變爲:n=2,C=7,體積{5,4},價值{20,10}。好了, 現在這個問題的解是什麼,肯定是:把體積爲5的寶石放入揹包 (然後剩下體積2,裝不下第二個寶石,只能眼睜睜看着它溜走),此時價值爲20。 這樣一來,我們發現,n=3時,放入揹包的是0號和2號寶石;當n=2時, 我們放入的是0號寶石。這並不是一個偶然,沒錯, 這就是傳說中的“全局最優解包含局部最優解”(n=2是n=3情況的一個局部子問題)。 繞了那麼大的圈子,你可能要問,這都哪跟哪啊?說好的狀態呢?說好的狀態轉移方程呢? 別急,它們已經呼之欲出了。

我們再把上面的例子理一下。當n=2時,我們要求的是前2個寶石, 裝到體積爲7的揹包裏能達到的最大價值;當n=3時,我們要求的是前3個寶石, 裝到體積爲10的揹包裏能達到的最大價值。有沒有發現它們其實是一個句式!OK, 讓我們形式化地表示一下它們, 定義d(i,j)爲前i個寶石裝到剩餘體積爲j的揹包裏能達到的最大價值。 那麼上面兩句話即爲:d(2, 7)和d(3, 10)。這樣看着真是爽多了, 而這兩個看着很爽的符號就是我們要找的狀態了。 即狀態d(i,j)表示前i個寶石裝到剩餘體積爲j的揹包裏能達到的最大價值。 上面那麼多的文字,用一句話概括就是:根據子問題定義狀態!你找到子問題, 狀態也就浮出水面了。而我們最終要求解的最大價值即爲d(n, C):前n個寶石 (0,1,2…,n-1)裝入剩餘容量爲C的揹包中的最大價值。狀態好不容易找到了, 狀態轉移方程呢?顧名思義,狀態轉移方程就是描述狀態是怎麼轉移的方程。 那麼回到例子,d(2, 7)和d(3, 10)是怎麼轉移的?來,我們來說說2號寶石 (記住寶石編號是從0開始的)。從d(2, 7)到d(3, 10)就隔了這個2號寶石。 它有兩種情況,裝或者不裝入揹包。如果裝入,在面對前2個寶石時, 揹包就只剩下體積7來裝它們,而相應的要加上2號寶石的價值12, d(3, 10)=d(2, 10-3)+12=d(2, 7)+12;如果不裝入,體積仍爲10,價值自然不變了, d(3, 10)=d(2, 10)。記住,d(3, 10)表示的是前3個寶石裝入到剩餘體積爲10 的揹包裏能達到的最大價值,既然是最大價值,就有d(3, 10)=max{ d(2, 10), d(2, 7)+12 }。好了,這條方程描述了狀態d(i, j)的一些關係, 沒錯,它就是狀態轉移方程了。把它形式化一下:d(i, j)=max{ d(i-1, j), d(i-1,j-V[i-1]) + W[i-1] }。注意討論前i個寶石裝入揹包的時候, 其實是在考查第i-1個寶石裝不裝入揹包(因爲寶石是從0開始編號的)。至此, 狀態和狀態轉移方程都已經有了。

程序及詳細講解


public class knapspace{
    public static void main(String[] args) {
        int N = 3 ;     // 寶石個數
        int C = 10 ;    // 書包容量
        int V[] = {0,5,3,4} ;       // 每個寶石的體積,這裏前面的0是爲了後面表示方便,即V[1]表示爲第一個寶石的體積,下同
        int W[] = {0,20,10,12} ;    // 每個寶石的價值
        int d[][] = new int [N+1][C+1] ;

        d[0][C] = 0 ;

        for (int i = 1; i <= N; i++) {
                for (int j= 1; j<=C; j++) {
                    if ( i > 0 && j>=V[i]) {
                    // 狀態轉移方程
                        d[i][j] = (d[i-1][j] > d[i-1][j-V[i]] + W[i]) ? d[i-1][j] : d[i-1][j-V[i]] + W[i] ;
                    }
                }
            }
            System.out.println(d[N][C]) ;   
    }
}

爲了說明情況 ,我們改下程序,讓每次執行都打印 d[i][j]看一看,每次都是怎麼變化的。

public class knapspace{
    public static void main(String[] args) {
        int N = 3 ;     // 寶石個數
        int C = 10 ;    // 書包容量
        int V[] = {0,5,3,4} ;       // 每個寶石的體積,這裏前面的0是爲了後面表示方便,即V[1]表示爲第一個寶石的體積,下同
        int W[] = {0,20,10,12} ;    // 每個寶石的價值
        int d[][] = new int [N+1][C+1] ;    // 狀態數組

        d[0][C] = 0 ;

        for (int i = 1; i <= N; i++) {
            System.out.println("*********  i="+ i +"  *********" ) ;
                for (int j= 1; j<=C; j++) {
                    if ( i > 0 && j>=V[i]) {
                        // 狀態轉移方程
                        d[i][j] = (d[i-1][j] > d[i-1][j-V[i]] + W[i]) ? d[i-1][j] : d[i-1][j-V[i]] + W[i] ;

                        System.out.println("\t---------- j="+j+"  d[i-1][j]="+d[i-1][j]+"   d[i-1][j-V[i]]+ W[i]="+(d[i-1][j-V[i]]+ W[i])+"   ---------") ;
                        // 打印每次保存狀態的數組
                        print_array(d) ;
                    }
                }
                System.out.println() ;
            }
    }
    // 打印保存數據的二位數組
    public static void print_array(int A[][]){
        System.out.println("=======================================================================") ;
        for (int i= 1;i<A.length ;i++ ) {
            for (int j = 1; j<A[0].length; j++) {
                System.out.print(A[i][j] + "\t") ;
            }
            System.out.println();
        }
        System.out.println("=======================================================================") ;
    }
}

我們來看打印出來的 i=1 時候的數據,表示只有第一個寶石的情況:
這裏寫圖片描述

一共三行,表示放三次寶石;10列,表示揹包的總體積大小。從程序中可以看到,當求解當前(i=1)最優解的時候,是將當前解的子結構與當前情況做比較(d[i-1][j] 、d[i-1][j-V[i]] + W[i])

d[i-1][j] 表示在體積爲j,不裝這塊寶石的時候,獲得的最大價值;此價值通過 i-1 的狀態獲得,即最優子結構。
d[i-1][j-V[i]] + W[i]: 表示體積在可以裝下第 i 塊寶石的時候,裝下這塊寶石的總價值。(d[i-1][j-V[i]]表示在裝下當前寶石的時候,剩餘體積的最優子解)

可以看到這裏進行了5次比較,從可以裝下當前寶石的體積 j=5 開始。

下面看一下 i=2 的情況:
這裏寫圖片描述
我們可以看到,i=2的時候,從 j=3 開始,因爲第二塊寶石的體積大小爲3,如果不放入第一塊,只放入第二塊的話, j=3是符合要求的。
第一行就是 i=1 的求解結果,這裏作爲 i=2 的最優子結構。

當體積 j<5時,只裝入寶石2(V[2] = 3)獲取的價值最大;
而在 j>5 的時候,將 d[i-1][j] 、d[i-1][j-V[i]] + W[i] 進行比較,即不裝當前寶石與裝入當前寶石的總價值比較,選出一個最好的結果。
其中 j>=8時,d[i-1][j] = 20(表示不裝當前寶石的最高價值,因爲此時二者只可裝下一個,所以選總價值最高的裝),d[i-1][j-V[i]] + W[i]=30(表示裝下當前寶石的最高價值,因爲此時二者均可裝下,所以總價值爲兩個寶石價值和),此時最優解爲30。

i=3 的情況:
這裏寫圖片描述
這個,按照上面的邏輯同樣可以分析清楚。

總結

對於初學者,動態規劃是比較難理解的,可以選擇多做幾題,多思考每一步是怎麼達到的。動態規劃的思想就是利用 空間換時間,開闢獨立的空間存儲最優子解,求新問題解=子問題+狀態轉移,這樣可以避免掉對於子問題的重複計算。在動態規劃中,狀態和狀態轉移方程式很重要的。
以上個人觀點,歡迎討論。

參考

動態規劃之揹包問題
Dynamic Programming - From Novice to Advanced
什麼是無後效性?
動態規劃

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