【背包九讲(一)】01背包问题




1. 基础的01背包问题

1.0 题目


N N N 件物品和一个容量为 V V V 的背包。第 i i i 件物品的体积是 w [ i ] w[i] w[i],价值是 v [ i ] v[i] v[i] ,求将哪些物品装入背包可使价值总和最大。


【输入格式】
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。

【输入样例】
4 5
1 2
2 4
3 4
4 5


【输出格式】
输出一个整数,表示最大价值。

【输出样例】
8

AcWing:01背包问题



1.1 解题方法一(二维)

解题思路1

01背包问题是一个典型的动态规划问题,可以将原问题转化成其子问题进行求解。

  • 【base case】:如果物品只有一个:能放得下就放,放不下背包中就不放任何东西。
  • 【dp数组】:定义一个二维的数组 dp[i][j] ,表示的含义为 —— 对于前 i 件物品,背包容量为 V 时,背包里装的东西的最大价值。
  • 【状态转移】:第 i 个物品的体积是否比整个背包的容积还大?
    • 【如果放不下】:那就不拿第 i i i 个物品,和只拿前 i-1 个一样。 dp[i][j] = dp[i - 1][j];
    • 【如果放得下】:那就再比较一下,
      • 取第 i 个,并取剩下容积的最大值;
      • 不取第 i 个,只拿前 i-1 个(和前面的状态相同)。



代码1

public class Backpack_1_2 {
    public int solution(int num, int maxVolumn, int[][] items) {
        if (num == 1 && items[0][1] <= maxVolumn) return items[1][1];
        if (num == 1) return 0;

        // 【取前 i 个物品,背包容量为 j 】时候的最大价值
        int[][] dp = new int[num + 1][maxVolumn + 1];

        for (int i = 1; i < num + 1; i++) {
            for (int j = 1; j < maxVolumn + 1; j++) {
                dp[i][j] = dp[i - 1][j];
                if (items[i][0] <= j) {     // 如果第 i 个物品装得下
                    dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - items[i][0]] + items[i][1]);
                }
            }
        }
        return dp[num][maxVolumn];
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int num = sc.nextInt();
        int maxVolumn = sc.nextInt();
        int[][] items = new int[num + 1][2];    // [体积,价值]
        for (int i = 1; i < num + 1; i++) {
            items[i][0] = sc.nextInt();
            items[i][1] = sc.nextInt();
        }

        Backpack_1_2 obj = new Backpack_1_2();
        System.out.println(obj.solution(num, maxVolumn, items));
    }
}

打印 dp[][] 数组(为了方便把第0行和第0列空出来了):

//背包容量为:
//0 1 2 3 4 5
  0 0 0 0 0 0 
  0 2 2 2 2 2 		// 只考虑第 1 个物品
  0 2 4 6 6 6 		// 只考虑第 1、2 个物品
  0 2 4 6 6 8 	    // 只考虑第 1、2、3 个物品
  0 2 4 6 6 8		// 考虑第 1、2、3、4 个物品




1.2 解题方法二(优化至一维)

解题思路2

从上面的 dp[][] 数组我们可以看出,实际上行与行之间是 递增 的关系(物品多了,组合显然也更多嘛,更容易从中找出更大价值的组合)。

因此,可以进一步将二维的 dp[][] 压缩成一维数组 dp[] ,即 直接把第 i 行的结果覆盖到第 i-1 行,这样只用维护一行数组就行了, 从而优化了空间复杂度。

同理,根据动态规划问题的三步走:

  • 【base case】:如果物品只有一个:能放得下就放,放不下背包中就不放任何东西。
  • 【dp数组】:定义一维的数组 dp[j] ,含义为 —— 总重量不超过 j 的情况下的最大价值。
  • 【状态转移】:对于前 i 个物品,考虑当拿了第 i 个时,剩下的空间分别要拿什么才能达到最大(依次遍历 dp[0]dp[剩余空间], 其中剩余空间总容量 - 第i个物品的体积

这里有一个要注意的点:内层的循环,j 必须从 maxVolumnitems[i][0] 递减 ,这是因为,要确保 dp[j - items[i][0]]i-1 状态的值,而不是 i 状态下的(同一轮外层for循环中算过的)。



代码2

public class Backpack_1_3 {
    public int solution(int num, int maxVolumn, int[][] items) {
        if (num == 1 && items[0][1] <= maxVolumn) return items[1][1];
        if (num == 1) return 0;

        // 【背包容量为 i 】时候的最大价值
        int[] dp = new int[maxVolumn + 1];

        for (int i = 1; i <= num; i++) {     // 对于前 i 个物品
            // 这里倒过来是因为:要保证dp[j - items[i][0]] 是i-1状态的值,而不是 i 状态下的
            for (int j = maxVolumn; j >= items[i][0]; j--) {     // 背包容量为 j
                dp[j] = Math.max(dp[j], dp[j - items[i][0]] + items[i][1]);
            }

        }
        return dp[maxVolumn];
    }

    public static void main(String[] args) {
        int num = 4;
        int maxVolumn = 5;
        int[][] items = {{0, 0}, {1, 2}, {2, 4}, {3, 4}, {4, 5}};

        Backpack_1_3 obj = new Backpack_1_3();
        System.out.println(obj.solution(num, maxVolumn, items));
    }
}

打印 dp[] 数组:

//背包容量为:
//0 1 2 3 4 5
  0 2 4 6 6 8 




2. 变型的01背包问题

在上面的题目中,题目给出了一个容量为 V V V 的背包,只要所拿的物品不超过这个容积就可以了,但不要求正好装满(不多不少)

如果现在将上面的基础01背包稍微改动一下,要求:背包必须要装满。 此时该怎么办呢?



解题思路

跟基础01背包代码相比,唯一要做的就是 改动一下dp数组的初始化 即可。除了 dp[0] == 0 ,其他的都要初始化为 -inf


为什么?

以一维 dp 数组的解法为例,在状态转移的过程中,
dp[j - items[i][0]] + items[i][1],前者如果

  • 正好能被凑齐,那和之前一样;
  • 凑不齐,那么它的 dp 值就会是 -inf ,在 Math.max() 中就不会被选到,从而排除了没有正好塞满背包(凑不齐)的情况。

举例说明:

【输入样例】
物品:
2 4
3 5
4 100

背包容量:
5

初始的dp数组:
[0, -inf, -inf, -inf, -inf, -inf]
第一轮外层for循环(i=1, j = 5 ~ 2):

dp[5] = max(dp[5], dp[3] + 4) = -inf
dp[4] = max(dp[4], dp[2] + 4) = -inf
dp[3] = max(dp[3], dp[1] + 4) = -inf
dp[2] = max(dp[2], 【dp[0] + 4) = 4

循环结束后的dp数组:
[0, -inf, 2, -inf, -inf, -inf]
第二轮外层for循环(i=2, j = 5 ~ 3):

dp[5] = max(dp[5], 【dp[2] + 5) = 4+5 = 9
dp[4] = max(dp[4], dp[1] + 5) = -inf
dp[3] = max(dp[3], 【dp[0] + 5) = 5

循环结束后的dp数组:
[0, -inf, 2, 5, -inf, 9]
第三轮外层for循环(i=3, j = 5 ~ 4):

dp[5] = max(【dp[5], dp[1] + 100) = 9
dp[4] = max(dp[4], 【dp[0] + 100) = 100

循环结束后的dp数组:
[0, -inf, 2, 5, 100, 9]

最后,当背包容量为5时(且要保证正好装满),只能取9。



代码

public int solution(int num, int maxVolumn, int[][] items) {
        if (num == 1 && items[0][1] <= maxVolumn) return items[1][1];
        if (num == 1) return 0;

        // 【背包容量为 i 】时候的最大价值
        int[] dp = new int[maxVolumn + 1];
        Arrays.fill(dp, Integer.MIN_VALUE);
        dp[0] = 0;

        for (int i = 1; i <= num; i++) {     // 对于前 i 个物品
            // 这里倒过来是因为:要保证dp[j - items[i][0]] 是i-1状态的值,而不是算过i情况下的
            for (int j = maxVolumn; j >= items[i][0]; j--) {     // 背包容量为 j
                dp[j] = Math.max(dp[j], dp[j - items[i][0]] + items[i][1]);
            }

        }

        for (var v: dp) {
            System.out.print(v + " ");
        }
        System.out.println();

        return dp[maxVolumn];
    }
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值