0/1背包的遍历顺序问题

正式开始之前回顾一下,二维0/1背包代码

// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
    for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
        if (j < weight[i]) dp[i][j] = dp[i - 1][j];
        else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

    }
}

0/1背包的遍历顺序

首先,总结写0/1背包遍历循环的关键:物品只能被放入一次背包,不能被多次放入背包。放在递推表达式上看,就是每次更新

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

在计算dp[j]的时候,不能重复使用 放入同一个物品 而计算出的旧dp[j]。

1、一维dp数组如何初始化

关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱

dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。

那么dp数组除了下标0的位置,初始为0,其他下标应该初始化多少呢?

看一下递归公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。

这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了

那么我假设物品价值都是大于0的,所以dp数组初始化的时候,都初始为0就可以了。

  1. 一维dp数组遍历顺序

代码如下:

for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

    }
}

这里大家发现和二维dp的写法中,遍历背包的顺序是不一样的!

二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。

为什么呢?

倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!

举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15

如果正序遍历

dp[1] = max(0,dp[1 - weight[0]] + value[0]) = 15

dp[2] = max(0,dp[2 - weight[0]] + value[0]) = 30

此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。

为什么倒序遍历,就可以保证物品只放入一次呢?

倒序就是先算dp[2]

dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)

dp[1] = dp[1 - weight[0]] + value[0] = 15

所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。

那么问题又来了,为什么二维dp数组遍历的时候不用倒序呢

因为对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖!

(如何这里读不懂,大家就要动手试一试了,空想还是不靠谱的,实践出真知!)

再来看看两个嵌套for循环的顺序,代码中是先遍历物品嵌套遍历背包容量,那可不可以先遍历背包容量嵌套遍历物品呢?

不可以!

因为一维dp的写法,背包容量一定是要倒序遍历(原因上面已经讲了),如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。

代码一:先倒序遍历背包 再正序遍历物品
for (int j = 10; j >= 0; j--) { // 遍历背包容量
    for (int i = 0; i < weight.size(); i++) { // 遍历物品
        if (j >= weight[i]) {
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
}
想象一下,也就是在x轴代表容量,y轴代表物品的二维表格遍历。


当dp[10] 确实不会被之前的dp[j]影响,但同时dp[10]只会放入一个价值最大的物品。
而小于10的j dp[j]可能受到前面计算出来的dp[j]影响  比如之前算出来的dp[7]大于 j=7这一轮的dp[7],但是也只会放入一件物品,那就是只装入之前weight=3的物品
最后的结果就是背包只放入了一个价值最大的物品。


或者是本轮循环遍历j的时候,dp[j - weight[i]] + value[i]比之前算的dp[j]大那么就会改装下这一轮价值最大的物品。
放反正遍历完后背包只装了一件物品。

代码二:先正序遍历背包 再正序遍历物品
for (int j = 0; j <= 10; j++) { // 遍历背包容量
    for (int i = 0; i < weight.size(); i++) { // 遍历物品
        if (j >= weight[i]) {
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
}

那么这样后续的dp[j]我使用前面行dp[j]的值,比如如果i重量最小,价值最大,
比如weight[i] = 1, value = 10,
dp[9] = max(dp[9], dp[8] + 10)
dp[10] = max(dp[10], dp[9] + 10)

那么每一次dp[j]都会加入i物品,最后导致同一物品被加入很多次。

所以一维dp数组的背包在遍历顺序上和二维其实是有很大差异的!,这一点大家一定要注意。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值