0/1揹包與完全揹包

揹包問題描述:

假設有一個固定容量的揹包,然後有許多具有價值屬性和重量屬性的物品,要求在不超過揹包最大容量的基礎上裝的物品的總價值最大。

假設共有N個物品,揹包的容量爲M,物品 i 的價值與重量分別爲 value[i] 和 weight[i]。dp[i][j] 爲可選物品爲一到i,揹包空間爲j時的最大價值。

0/1揹包

0/1揹包爲最基礎的揹包問題,顧名思義,所有的物品只有一件,只有拿或不拿兩種選擇,dp[i][j]的表達式如下:

dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])

最大價值爲選i物品和不選i物品的最大值。

實現代碼如下:

    /**
     * N : 物品個數
     * M : 揹包容量
     */
    int N = 4;
    int M = 15;
    int[] weight = {0, 2, 4, 5, 9}; 
    int[] value = {0, 3, 5, 8, 10};
    public int zeroOneBackpack() {
        int[][] dp = new int[N + 1][M + 1];
        for(int i = 1; i <= N; i++) {
            for(int j = 1; j <= M; j++) {
                if(j < weight[i]) {
                    dp[i][j] = dp[i - 1][j];
                }else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
                }
            }
        }
        return dp[N][M];
    }

從上述轉移方程中可以看出來,dp[i][j] 的取值只與其上一行的dp值有關,因此使用一個一維數組存儲上一行的結果,可以將二維動態規劃優化爲一維。轉移方程變爲:

dp[j] = max(d[j], dp[j - weight[i]] + value[i])

但是此時需要注意由於之前二維dp[i][j]計算中用到了dp[i - 1][j - weight[i]],若原地修改的還是按照從左至右的順序,由於j - weight[i]在j前面,如此會出現計算dp[i][j]時用到的是dp[i][j - weight[i]]。代碼如下:

    public int zeroOneBackpack() {
        int[] dp = new int[M + 1];
        for(int i = 1; i <= N; i++) {
            for(int j = M; j >= 0; j--) {
                if(j < weight[i]) {
                    dp[j] = dp[j];
                }else {
                    dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
                }
            }
        }
        return dp[M];
    }

完全揹包

其與0/1揹包的不同在於,所有的物品的數量是無窮大,一件物品可以拿多次。在0/1揹包中,dp[i][j]的值爲拿該物品和不拿該物品兩種可能中價值最大的,通過類比可以得到完全揹包中的dp[i][j] 的值應該爲拿該物品,拿一件, 拿兩件......拿j / weight[i]件中的價值最大的。其數學表達式如下:

dp[i][j] = max(dp[i - 1][j - k * weight[i]] + k * value[i]) \; \; k = 0,1,...j / weight[i]

實現代碼如下:

    public int completeBackpack() {
        int[][] dp = new int[N + 1][M + 1];
        for(int i = 1; i <= N; i++) {
            for(int j = 1; j <= M; j++) {
                for(int k = 0; k <= j / weight[i]; k++) {
                    dp[i][j] = Math.max(dp[i][j], 
                        dp[i - 1][j - k * weight[i]] + k * value[i]);
                }
            }
        }
        return dp[N][M];
    }

上述解法的時間複雜度爲O(N * M * M)。

對上述解法的優化推導如下:

      dp[i][j] = max(dp[i - 1][j - k * weight[i]] + k * value[i])\; k = 0,1,...j / weight[i]

\Rightarrow dp[i][j - weight[i]] = max(dp[i - 1][j - (k + 1) * weight[i]] + k * value[i]) \;k = 0,...j / weight[i] - 1

\Rightarrow dp[i][j - weight[i]] = max(dp[i - 1][j - (k + 1) * weight[i]] + (k + 1) * value[i]) - value[i] \; \; k = 0,1,...j / weight[i] - 1

\Rightarrow dp[i][j] =max(dp[i - 1][j - k * weight[i]] + k * value[i]) - value[i] \; \; k = 1,...j / weight[i]

             \Rightarrow dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + values[i])

實現代碼如下:

    public int completeBackpack() {
        int[][] dp = new int[N + 1][M + 1];
        for(int i = 1; i <= N; i++) {
            for(int j = 1; j <= M; j++) {
                if(j < weight[i]) {
                    dp[i][j] = dp[i - 1][j];
                }else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);
                }
            }
        }
        return dp[N][M];
    }

此時時間複雜度降爲了O(N * M),我們在同0/1揹包那樣,將額外空間複雜度也降爲O(M),奇妙的事情發生了,優化後的轉移方程也爲:

dp[j] = max(d[j], dp[j - weight[i]] + value[i])

當然跟之前不同了,上述公式中的dp[j - weight[i]]的值取的是本輪的新計算的值,而0/1揹包中則爲上一輪的舊值,因此從前往後遍歷即可,實現代碼如下:

    public int completeBackpack() {
        int[] dp = new int[M + 1];
        for(int i = 1; i <= N; i++) {
            for(int j = 1; j <= M; j++) {
                if(j < weight[i]) {
                    dp[j] = dp[j];
                }else {
                    dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
                }
            }
        }
        return dp[M];
    }

 

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