01背包总结

题目

有 n 件物品和一个最多能背重量为 w 的背包。第i件物品的重量是weight[i],得到的价值是value[i]每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

比如有 3 个物品,重量一次为:1,3, 4。价值依次为:15,20,30,背包最大重量为4,如何装物品使得总价值最大?

递推公式

对于背包问题,有一种写法, 是使用二维数组,即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。

我们用一个表格来表示dp[i][j]数组,纵向表示 0 ~ i 个物品,横向表示 0 ~ j 的重量。每一个格子表示物品i在当前背包容量下的最大价值。物品 0 表示不放物品,重量 0 表示背包的容量为 0。

物品 i \ 背包容量 j 0 1 2 3 4
物品0 0 0 0 0 0
物品1(1,15) 0 15 15 15 15
物品2(3,20) 0 15 15 20 35
物品3(4,30) 0 15 15 20 35

显然,当不放物品或背包容量为 0 时,背包里物品的最大价值是 0。然后,每件物品,都有放与不放两种情况,我们选择其中教大的一种情况。

对于物品 1:

当背包容量为 1 ,因为 j > i,所以格子是 15,后面也都是 15。

对于物品 2:

  1. 当容量为 1,不放物品 2 ,则背包里只有上一次放的物品,那么价值则为 dp[i - 1][j],也就是15;放物品的话,因为此时背包容量小于物品重量,即 j < weight[i - 1],所以价值是 0。二选一则格子填 15。
  2. 当容量为 2,同上,也是 15。
  3. 当容量为 3, 此时物品 2 能放的下。当不放物品时,总价值等于上个物品的总价值 15,当放物品时,说明前一个物品(i - 1)对应的背包容量为j - weigh[i - 1],即放此物品时,前一个物品的总价值只能是dp[i - 1][j - weight[i - 1]]。前一个物品的总价值是dp[2 - 1][3 - 3] = 0,那么此次放入物品的总价值 = 20 + 0,20 > 15 选择20。
  4. 当容量为 4, 此时物品 2 能放的下。当不放物品时,总价值等于上个物品的总价值 15,当放物品时,说明前一个物品(i - 1)对应的背包容量为j - weigh[i],即放此物品时,上一个物品的总价值只能是dp[i - 1][j - weight[i - 1]]。前一个物品的总价值是dp[2 - 1][4 - 3] = 15,那么此次放入物品的总价值 = 20 + 15,35 > 15选择35。
  5. 依此类推......

特别说明:物品 i 对应的重量是 weight[i - 1] ,因为物品 i 从 0 开始的,多一个虚拟物品。

确定递推公式

那么可以有两个方向推出来dp[i][j]

  • 不放物品 i:由dp[i - 1][j]推出,即背包容量为 j,里面不放物品 i 的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品 i 的重量大于背包 j 的重量时,物品 i 无法放进背包中,所以背包内的价值依然和前面相同。)
  • 放物品 i:由dp[i - 1][j - weight[i - 1]]推出,dp[i - 1][j - weight[i - 1]] 为背包容量为j - weight[i - 1]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i - 1]] + value[i] ,就是背包放物品i得到的最大价值。

二选一,则递推公式为:dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i])

代码

public class Knapsack01 {
    public static void main(String[] args) {
        int w = 4; // 背包容量
        int[] weight = {1, 3, 4};
        int[] value = {15, 20, 30};
        int n = weight.length; // 物品的数量

        // 创建dp二维数组, 数组的横向是当前背包重量,范围 0 ~ w, 纵向是物品编号,范围 0 ~ n。
        // 纵向0 表示 表示不放物品,横向0表示背包容量为0。因为把物品0和重量0算进去了,所以数组范围+1
        int[][] dp = new int[n + 1][w + 1];

        // 填充dp数组
        for (int i = 1; i <= n; i++) { // 物品编号从1开始,因为0的情况价值是0,不考虑了
            for (int j = 1; j <= w; j++) { // 背包容量从1开始,因为0的情况价值是0,不考虑了
                // 这里注意,物品编号i从1开始,但是重量数组weight[]的下标从0开始的,第i个物品对应的重量weight[i - 1]
                // 包括下面的value[i -1]也是一样的道理,物品i在value数组中的值是value[i-1]
                if (j < weight[i - 1]) {
                    /*
                     * 当前背包的容量都没有当前物品i大的时候,是不放物品i的
                     * 那么前i-1个物品能放下的最大价值就是当前情况的最大价值
                     */
                    dp[i][j] = dp[i - 1][j];
                } else {
                    /*
                     * 当前背包的容量可以放下物品i
                     * 那么此时分两种情况:
                     *    1、不放物品i
                     *    2、放物品i
                     * 比较这两种情况下,哪种背包中物品的最大价值最大
                     */
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i -1]);
                }
            }
        }

        // 最大价值是:
        System.out.println(dp[n][w]);

        // dp 数组信息是:
        for (int i = 0; i <= n; i++) {
            for (int j = 0; j <= w; j++) {
                System.out.print(dp[i][j] + "\t");
            }
            System.out.println("\n");
        }
    }
}

怎么找出物品编号

怎么找出产生最大价值的物品编号?回溯法。

// 从最右下角开始遍历,
for (int i = n; i > 0; i--) {
    for (int j = w; j > 0; j--) {
        // 最右边的格子表示在最大背包容量下的最大价值
        // 如果当前格子比上一个格子的值大,说明当前物品放进了背包
        // 如果是相等则说明当前物品没有放进背包
        if (dp[i][j] > dp[i - 1][j]) {
            System.out.println(i);
            break;
        }
    }
}

参考资料

posted @ 2024-05-12 18:34  yfhu  阅读(15)  评论(0编辑  收藏  举报