揹包算法研究

揹包算法

問題描述

假定揹包的最大容量爲W,N件物品,每件物品都有自己的價值和重量,將物品放入揹包中,使得揹包內物品的總價值最大。

有這樣一個場景——小偷在屋裏偷東西,他帶着一隻揹包,屋子裏物品數量有限——每件物品都有一定的重量和價值——珠寶重量輕但價值高,桌子重但價值低,最重要的是小偷揹包容量有限.很顯然,他不能把桌子分成兩份或者帶走珠寶的3/4.對於一件物品他只能帶走或不帶走。

實例:

Knapsack Max weight : W = 10 (units)  //揹包總重量爲10
Total items         : N = 4           //最多容納4件
Values of items     : V[] = {10,40,30,50} 
Weight of items     : w[] = {5,4,6,3}

從實例數據大致估算一下,最大重量爲10時,揹包能容納的物品最大價值爲50+40=90,重量爲7.

解決方法

最佳的解決方法是使用動態規劃——先得到該問題的局部解然後擴展到全局問題解。

構建物品X在不同重量時的價值數組V(Value數組)

V[N][W] = 4 rows * 10 columns

該矩陣中的每個值的求解都代表一個更小的揹包問題。

初始情況一:對於第0列,它的含義是揹包的容量爲0,此時物品的價值?沒有,因此,第一列都填充0.

初始情況二:對於第0行,它的含義是屋內沒有物品。那麼沒有任何物品的揹包裏的價值多少呢?還是沒有!所有都是0。(WeightW,表示總重爲W時的情況)

步驟

1、現在,開始填入數組每一行的值。第1行第1列表示什麼含義呢?對於第一個物品,可以把重量爲1的該物品放入揹包嗎?不行。第一個物品的重量是5,大於0。因此,填入0。實際上直到第5列(重量5)之前都應該填入0。

2、對於第1行的第5列(重量5),意味着將物品1放入揹包。填入10(注意,這是Value數組):

3、繼續,對於第6列,我們可以再放入重量爲1(重量值-物品的重量)的物品嗎。我們現在只考慮物品1。由於我們加入物品1之後就不能再加入額外的重量,可以很直觀地看到其餘的列都應該還是相同的值。

4、接着,有意思的事情出現,在第3行第4列,此時重量爲4。作如下判斷:

  1. 可以放入物品2嗎?——可以,物品2的重量爲4.
  2. 不加入物品2的話,當前已有物品的重量的Value值是否最大——查看相同重量時的前一行的值。
  3. 前一行的值爲0,重量4時不能放入物品1.
  4. 在這個重量時可以放入兩件物品使得價值最大碼?——不能,此時重量減去物品2的重量後爲0.

爲什麼是前一行

簡單來說,重量爲4的前一行的值本身就是個更小的揹包問題解,它的含義是到該重量時揹包內物品的最大價值(通過遍歷物品得到)。

舉個例子:

  1. 當前物品價值 = 40
  2. 當前物品重量 = 4
  3. 剩餘重量 = 4-4 = 0
  4. 查看上面的行(物品1或者其餘行的值)。剩餘容量爲0時,可以再容納物品1嗎?對於該給定的重量值上面的行還有任何值嗎?

計算過程如下:

1) 計算不放入該物品時該重量的最大價值:

previous row, same weight = 0
=> V[item-1][weight]

2) 計算當前物品的價值 + 可以容納的剩餘重量的價值

Value of current item
+ value in previous row with weight 4 (total weight until now (4) - weight of the current item (4))
=> val[item-1] + V[item-1][weight-wt[item-1]]

找到二者之中的最大值40(0和40)。

3) 下一次最重要的位置爲第2行第9列。意味着此時重量爲9,放入兩件物品。根據示例數據現在可以放入兩件物品。我們作了以下判斷:

The value of the current item = 40
The weight of the current item = 4
The weight that is left over = 9 - 4 = 5
Check the row above.  At the remaining weight 5, are we able to accommodate Item 1.

計算如下:

不加入該物品時該重量的最大價值:
previous row, same weight = 10

計算當前物品的價值+可以容納的剩餘重量的價值

Value of current item (40)
+ value in previous row with weight 5 (total weight until now (9) - weight of the current item (4)) 

= 10

10 vs 50 = 50。

解決了所有的子問題之後,返回V[N][W]的值——4件物品重量爲10時:

複雜度

解法的複雜度非常直觀。在N次循環中有W次循環 => O(NW)

實現

Java代碼實現:

class Knapsack {
    public static void main(String[] args) throws Exception {
        int val[] = {10, 40, 30, 50};
        int wt[] = {5, 4, 6, 3};
        int W = 10;

        System.out.println(knapsack(val, wt, W));
    }

    public static int knapsack(int val[], int wt[], int W) {
        //Get the total number of items. 
        //Could be wt.length or val.length. Doesn't matter
        int N = wt.length; 

        //Create a matrix. 
        //Items are in rows and weight at in columns +1 on each side
        int[][] V = new int[N + 1][W + 1]; 

        //What if the knapsack's capacity is 0 - Set
        //all columns at row 0 to be 0
        for (int col = 0; col <= W; col++) {
            V[0][col] = 0;
        }

        //What if there are no items at home.  
        //Fill the first row with 0
        for (int row = 0; row <= N; row++) {
            V[row][0] = 0;
        }

        for (int item=1;item<=N;item++){
            //Let's fill the values row by row
            for (int weight=1;weight<=W;weight++){
                //Is the current items weight less
                //than or equal to running weight
                if (wt[item-1]<=weight){
                    //Given a weight, check if the value of the current 
                    //item + value of the item that we could afford 
                    //with the remaining weight is greater than the value
                    //without the current item itself,注意區分val[item-1]和V[item-1][weight]的取值,val[item-1]值是數組中的值,代表當前項值,V[item-1][weight]的值是表格中,縱向上,當前項的前一項的值。
                    V[item][weight]=Math.max (val[item-1]+V[item-1][weight-wt[item-1]], V[item-1][weight]);
                }
                else {
                    //If the current item's weight is more than the
                    //running weight, just carry forward the value
                    //without the current item
                    V[item][weight]=V[item-1][weight];
                }
            }

        }

        //Printing the matrix
        for (int[] rows : V) {
            for (int col : rows) {
                System.out.format("%5d", col);
            }
            System.out.println();
        }

        return V[N][W];
    }
}

動態規劃論述

動態規劃(Dynamic programming),簡稱DP,是指對某一類問題解決方法。重點在於如何鑑定“某一類問題”是動態規劃可解的,而不是糾結解決方法是遞歸還是遞推。

怎麼鑑定DP可解的問題,可以從計算機的工作方式說起。計算機本質是一個狀態機,內存中存儲的所有數據構成了當前的狀態,CPU只是利用當前的狀態計算出下一個狀態(不要糾結硬盤之類的外部存儲,就算考慮他們也只是擴大了狀態的存儲容量,並不能改變下一個狀態只能從當前狀態計算出來這一條鐵律)。

當你企圖用計算機解決一個問題,其實就是在思考如何將這個問題表達成狀態(用哪些變量存儲哪些數據),如何在狀態中轉移(怎麼根據一些變量計算出另一些變量)。所謂的“空間複雜度”,就是爲了支持你的計算所必須存儲的狀態最多有多少,所謂“時間複雜度”,就是從初始窗臺到最終狀態,中間需要多少步。

比如:我想計算第100個斐波那契數列,每一個斐波那契數列就是這個問題的一個狀態,每次求解一個新數,只需之前的兩個狀態。所以同一時刻,最多隻需保存兩個狀態,空間複雜度就是常數。每計算一個新狀態所需要的時間也是常數,且狀態是線性遞增的,所以時間複雜度也是線性的。

這種狀態計算很直接,只需按照固定模式從舊狀態計算新狀態 a[i]=a[i-1]+a[i-2],無需考慮更多狀態,也不需要對舊狀態進行選擇。這樣的解法,我們稱之爲“遞推”。

斐波那契數列的例子過於簡單,以至於忽視了階段的概念,所謂階段是指隨着問題的解決,在同一時刻可能得到不同的狀態集合。斐波那契數列中,每一步會計算一個新數字,所以每個階段只有一個狀態。

想像另一個問題場景:假如把你放在一個圍棋棋盤的某一點,你每一步只能走一格,因爲你可以東西南北隨便走,所以當你同樣走幾步可能會處於很多個不同的位置。從頭開始走了幾步就是第幾個階段,走了n步可能處於的位置稱爲一個狀態,走了這n步所有可能到達的位置集合就是這個階段所有可能的狀態。

問題來了,有了階段之後,計算新狀態可能會遇到各種奇葩情況,針對不同情況,就需要不同的算法。

假如問題有n個階段,每個階段都有多個狀態,不同階段的狀態數量可以不同,一個階段的狀態可以得到下個階段所有狀態中的幾個。要計算出最終階段的狀態數,自然要經歷之前每個階段的某些狀態。

好消息是,有時候我們並不需要真的計算所有狀態,比如這樣一個簡單的棋盤問題:從棋盤的左上角到達右下角最短需要幾步。答案很明顯,提出這個問題,是爲了幫助我們理解階段和狀態。某個階段確實可以有多個狀態,正如這個問題中走n步可以走到很多位置一樣。但是同樣n步中,有哪些位置可以讓我們在第n+1中走的最遠呢?沒錯,正是第n步中走的最遠的位置。簡單點就是:下一步最優是從當前最優得到的。所以爲了計算最終的最優解,只需要存儲每一步的最優解即可,解決符合這種性質的問題算法叫做“貪心”.如果只看最優狀態之間的計算過程,是不是和斐波那契數列的計算過程很像,所以計算的方法叫“遞推”。

既然問題都是可以劃分成階段和狀態。我們一下子解決了一大類問題:一個階段的最優可以由前一個階段的最優得到。

如果一個階段的最優無法用前一個階段的最優得到呢?

再來一個迷宮的例子。在計算從起點到終點的最短路線時,你不能只保存當前階段的狀態,因爲題目要求你最短,所以必須知道之前走過的所有位置。因爲即便當前在的位置不變,之前的路線不同也會影響你之後走的路線。這時你需要保存的是之前每個階段所經歷的那個狀態,根據這些信息才能計算出下一個狀態。

每個階段的狀態或許不多,但是每個狀態都可以轉移到下一階段的多個狀態,所以解的複雜度是指數的,因此時間複雜度也是指數。插播一句,之前的路線會影響到下一步的選擇,這種情況稱之爲“後效性”。

迷宮的例子,解決方法太暴力,要避免如何的暴力,契機就在於後效性。

有一類問題,看似需要之前的所有狀態,其實不用,使用最長上升子序列的例子來說明爲什麼不需要暴力搜索,進而引出動態規劃的思路。

先來看一個動態規劃的教學必備提:

給定一個數列,長度爲N,
求這個數列的最長上升(遞增)子數列(LIS)的長度.
以
    1 7 2 8 3 4
爲例。
這個數列的最長遞增子數列是 1 2 3 4,長度爲4;
次長的長度爲3, 包括 1 7 8; 1 2 3 等.

假裝我們年幼無知,想用搜索去尋找最長上升子序列。怎麼搜索呢?需要從頭到尾依次枚舉是否選擇當前的數字,每選定一個數字就要去看看是不是滿足“上升”的性質,這裏第i個階段就是思考是否要選擇第i個數,第i個階段有兩個狀態,分別是選和不選。依稀出現了剛剛迷宮找路的影子。等等,容我想想,每當我決定要選擇當前數字的時候,只需要和之前選定的一個數字比較就行了。這是和之前迷宮問題本質的不同,可以縱容我們不需要記錄之前的所有狀態啊。既然我們的選擇已經不受之前狀態組合的影響,那時間複雜度自然不是指數了,雖然我們不在乎某序列之前都是什麼元素,但還是需要知道這個序列的長度。所以只需要記錄以某個元素結尾的LIS長度就好。因此第i個階段的最優解只是由當前第i-1個階段的最優解得到,然後就得到了DP方程

LIS(i) = max{LIS(j) + 1}  j < i and a[j] < a[i]

所以一個問題,該用遞推、貪心、搜索還是動態規劃,完全是由這個問題本身階段間狀態的轉移方式決定的。

1. 每個階段只有一個狀態——遞推。
2. 每個階段的最優狀態都是由上一個階段的最優狀態得到——貪心。
3. 每個階段的最優狀態是由之前所有階段的狀態組合得到——搜索
4. 每個階段的最優狀態可以從之前某個階段的某個或某些狀態直接得到,而不管之前這個狀態是如何得到——動態規劃。

每個階段的最優狀態可以從之前某個階段的某個或某些狀態直接得到,這個性質叫“最優子結構”。而不管之前這個狀態是如何得到,這個性質叫“無後效性”。

另:其實動態規劃中的最優狀態的說法容易產生誤導,以爲只需要計算最優狀態就好,LIS問題確實如何,轉移時只用到了每個階段“選”的狀態。但實際上,有的問題往往需要對每個階段的所有狀態都算出一個最優值,然後根據這些最優值再來找最優狀態。然後根據這些最優值再來找最優狀態。比如揹包問題,就需要對前i個包(階段)容量爲j時(狀態)計算出最大價值,然後在最後一個階段中的所有狀態中找到最優值。

狀態轉移方程:
f(n,m)=max{f(n-1,m), f(n-1,m-w[n])+P(n,m)}

原文鏈接: javacodegeeks
譯文鏈接: http://www.importnew.com/13072.html

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