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];
    }

 

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