Dynamic Programming学习笔记 (7) - 0/1背包问题

241 阅读2分钟

0/1背包问题是个经典的DP应用题,其一般表述为:给定一个背包,最多可以承重一定的重量,有若干不同的物件可以装入这个背包中,每个物件都有各自的重量和价值,问装入背包的物件的价值总和的最大值。这里的0/1指的就是每个物件有两种可能的状态:装入背包或不装入背包。

实例如下:

背包最大承重:5
物件重量:{ 2, 3, 1, 4 }
物件价值:{ 4, 5, 3, 7 }

答案是挑选最后两项,其重量之和为5,刚好等于背包的最大承重,其价值总和为10, 是所有可能的组合中的最大值。

DP解题的思路如下:

每个物件存在两种可能,第一种,该物件的重量小于或等于背包当前的剩余承重,因此可以装入背包;第二种,该物件的重量大于背包当前的剩余承重,因此不可以装入背包。如果是第一种情况,那又有两种选择,装入背包或不装入背包,如果装入背包,那么该物件的价值就被计入价值总和之中,同时背包剩余的承重也相应减少;如果不装入背包,那么价值总和不变,背包剩余的承重也没有变化。如果是第二种情况,那我们没有选择,只能不把该物件装入背包。整个的计算过程就是对给定的各个物件依次计算不同情况下下的最优值,直到获得全局的最优解。这种递归关系的数学表达式就是

F(k, w) 
   = 0,  k >= N or w <= 0
   = F(k + 1, w), w < weight[k]
   = max(F(k + 1, w), value[k] + F(k + 1, w - weight[k])

这里k是0开始的数组下标,w是背包剩余的承重量,N是物件的件数,value是物件价值数组,weight是物件重量数组,F(k, w)返回的就是当可选的物件为[k, k + 1, k + 2 ... N),背包可承受的重量为w时的最大价值之和。由此我们可以画出上面这个实例的递归树。

007.png

根据以上的表达式,我们可以定义一个二维的DP数组,一维是各个物件,另一维是背包可承受的各个重量,然后我们可以使用双重循环依据物件数组下标的顺序从大到小依次计算DP数组中的每个元素,最终答案就在DP[0][W]中,这里的W是背包最多承重的重量,DP[0][W]代表的就是原始问题状态(所有物件可选,背包剩余承重最大)下的最优解。

Java代码如下

class BottomUp {
    public int solveKnapsack(int[] weight, int[] profit, int W) {
        int N = weight.length;

        int[][] dp = new int[N + 1][W + 1];

        for (int i = N-1; i >=0; i --) {
            for (int c = 1; c <= W; c ++) {
                if (c < weight[i]) {
                    dp[i][c] = dp[i + 1][c];
                } else {
                    int a = dp[i + 1][c];
                    int b = profit[i] + dp[i + 1][c - weight[i]];
                    dp[i][c] = Math.max(a, b);
                }
            }
        }
        return dp[0][W];
    }
}