1. 程式人生 > >動態規劃-背包問題 Knapsack

動態規劃-背包問題 Knapsack

mat system dir 角度 兩個 代碼 選擇 每次 空間復雜度

2018-03-15 13:11:12

背包問題(Knapsack problem)是一種組合優化的NP完全問題。問題可以描述為:給定一組物品,每種物品都有自己的重量和價格,在限定的總重量內,我們如何選擇,才能使得物品的總價格最高。問題的名稱來源於如何選擇最合適的物品放置於給定背包中。

相似問題經常出現在商業、組合數學,計算復雜性理論、密碼學和應用數學等領域中。

一、0/1背包問題

背包問題是個NPC問題,01背包可以通過動態規劃算法在偽多項式時間內給出解。

0/1背包問題的特點是,每種物品僅僅有一件,且需要選擇放或者不放。

在0/1背包問題中,物品i或者被裝入背包,或者不被裝入背包,設xi表示物品i裝入背包的情況,則當xi=0時,表示物品i沒有被裝入背包,xi=1時,表示物品i被裝入背包。根據問題的要求,有如下約束條件和目標函數:

技術分享圖片

技術分享圖片

於是,問題歸結為尋找一個滿足約束條件式2.1,並使目標函數式2.2達到最大的解向量X=(x1, x2, …, xn)。

0/1背包問題可以看作是決策一個序列(x1, x2, …, xn),對任一變量xi的決策是決定xi=1還是xi=0。在對xi-1決策後,已確定了(x1, …, xi-1),在決策xi時,問題處於下列兩種狀態之一:
(1)背包容量不足以裝入物品i,則xi=0,背包不增加價值;
(2)背包容量可以裝入物品i,則xi=1,背包的價值增加了vi。
這兩種情況下背包價值的最大者應該是對xi決策後的背包價值。令V(i, j)表示在前i(1≤i≤n)個物品中能夠裝入容量為j(1≤j≤C)的背包中的物品的最大值

,則可以得到如下動態規劃函數:

技術分享圖片

式2.3表明:把前面i個物品裝入容量為0的背包和把0個物品裝入容量為j的背包,得到的價值均為0。
式2.4的第一個式子表明:如果第i個物品的重量大於背包的容量,則裝入前i個物品得到的最大價值和裝入前i-1個物品得到的最大價值是相同的,即物品i不能裝入背包;第二個式子表明:如果第i個物品的重量小於背包的容量,則會有以下兩種情況:
(1)如果把第i個物品裝入背包,則背包中物品的價值等於把前i-1個物品裝入容量為j-wi的背包中的價值加上第i個物品的價值vi;
(2)如果第i個物品沒有裝入背包,則背包中物品的價值就等於把前i-1個物品裝入容量為j的背包中所取得的價值。顯然,取二者中價值較大者作為把前i個物品裝入容量為j的背包中的最優解。

舉個例子:

例如,有5個物品,其重量分別是{2, 2, 6, 5, 4},價值分別為{6, 3, 5, 4, 6},背包的容量為10。
根據動態規劃函數,用一個(n+1)×(C+1)的二維表V,V[i][j]表示把前i個物品裝入容量為j的背包中獲得的最大價值。

第一階段,只裝入前1個物品,確定在各種情況下的背包能夠得到的最大價值;
第二階段,只裝入前2個物品,確定在各種情況下的背包能夠得到的最大價值;
依此類推,直到第n個階段。最後,V(n,C)便是在容量為C的背包中裝入n個物品
時取得的最大價值。

技術分享圖片

如何確定裝入背包的具體物品?

從V(n,C)的值向前推,如果V(n,C)>V(n-1,C),表明第n個物品被裝入背包,前n-1個物品被裝入容量為C-wn的背包中;否則,第n個物品沒有被裝入背包,前n-1個物品被裝入容量為C的背包中。依此類推,直到確定第1個物品是否被裝入背包中為止。由此,得到如下函數:

技術分享圖片

技術分享圖片

public class Knapsack {
    static int knapsack(int[] v, int[] w, int W) {
        int n = v.length;
        int[][] V = new int[n + 1][W + 1];
        int[] x = new int[n + 1];
        for (int i = 0; i < W + 1; i++) {
            V[0][i] = 0;
        }
        for (int i = 0; i < n + 1; i++) {
            V[i][0] = 0;
        }

        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= W; j++) {
                if (j < w[i - 1]) V[i][j] = V[i - 1][j];
                else V[i][j] = Math.max(V[i - 1][j - w[i - 1]] + v[i - 1], V[i - 1][j]);
            }
        }

        int j = W;
        for (int i = n; i >= 1; i--) {
            if (V[i][j] == V[i - 1][j]) x[i] = 0;
            else {
                j -= w[i - 1];
                x[i] = 1;
            }
        }

        for (int i = 1; i <= n; i++) {
            System.out.println(x[i]);
        }
        return V[n][W];
    }

    public static void main(String[] args) {
        System.out.println(knapsack(new int[]{6, 3, 5, 4, 6}, new int[]{2, 2, 6, 5, 4}, 10));
    }
}

相關的優化處理:

時間復雜度已經無法進一步進行優化了,但是空間復雜度還是有優化余地的,通過遞推式可以看到,每次下一行的值的產生僅僅依賴於上一行的前面兩個值,因此,我們可以將二維數組優化成一維數組進行存儲。

    static int polish(int[] v, int[] w, int W){
        int n = v.length;
        int[] m = new int[W + 1];
        m[0] = 0;
        for (int i = 1; i <= n; i++) {
            for (int j = W; j >= w[i - 1]; j--) {
                m[j] = Math.max(m[j - w[i - 1]] + v[i - 1], m[j]);
            }
        }
        return m[W];
    }

另外,在初始化的時候,如果是題目沒有要求必須得最終裝滿背包,則直接使用上述代碼即可,如果題目中指出必須裝滿背包,則在初始化的時候,除了V[0][0] = 0外,其余的0件物品,j個重量,抑或j個重量,0件物品都是不滿足裝滿背包的條件的,應該初始化為負無窮大。

二、完全背包問題

完全背包問題同樣給出了n件物品的重量和價值,並且給出了背包的大小W,但是和0/1背包不同的是,在完全背包問題中,每件物品可以選1,2,3...直到背包放不下為止。

完全背包是0/1背包問題的一個擴展,也同樣是一個非常經典的問題。

運用類比的思想,我們可以將完全背包轉化成0/1背包,具體的轉化可以有下面兩種方式:

1、將每件物品看成W/w[i]件,價值不變;

2、將每件物品看成v[i]*2^k,重量為w[i]*2^k,想法就是利用二進制的角度看問題,任何多種選擇都可以通過這些二進制數相加得到,這種方法的分解個數顯然要小很多,非常聰明。

如果從遞推式的角度來解決問題,可以得到一個非常好的解答:

V[i][j] = max{V[i - 1][j], V[i][j - w[i]] + v[i]}

對於每一個V[i][j]都可以看成要麽不選擇第i件,要麽選擇第i件且可以多選,那麽就可以很容易的得到上述的遞推式。

下面使用一維數組進行實現,你會發現除了內層的順序變了,其他的都沒有改變。

    static int polish(int[] v, int[] w, int W){
        int n = v.length;
        int[] m = new int[W + 1];
        m[0] = 0;
        for (int i = 1; i <= n; i++) {
            for (int j = w[i - 1]; j <= W; j--) {
                m[j] = Math.max(m[j - w[i - 1]] + v[i - 1], m[j]);
            }
        }
        return m[W];
    }

動態規劃-背包問題 Knapsack