01背包和完全背包(java)

所有的w和v数组下标都按从1开始了,方便描述问题,需要改就在用到w和v的地方下标-1就行。

01背包

给定n个物品,体重分别为w1,w2,w3…,价值分别为v1,v2,v3…,且每个物品只有一件,现在有一个容量为C的背包,求最多能装价值多大的东西。

通过枚举物品种类和背包的容量来解决该问题,现在假设有4件物品,对应的体积和价值如下表所示,背包的容量为10。

i1234
w(体积)10345
v(价值)3467

dp状态表如下所示,dp[i][j]表示当前只有i件物品,且背包容量为j时,所能装的最大价值。如果j >= w[i] ,表示当前的容量可以放下物品i,那么此时可以选择放,也可以选择不放:

//                  不放物品i       放物品i
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);

如果j < w[i] ,表示当前的容量放不下物品i,那么此时dp[i][j] = dp[i - 1][j],保持上一层的状态就可以。

初始状态dp[0][j],dp[i][0]均为0,分别表示不装任何物品和背包容量为0时的价值只能是0。这样也是为了避免讨论边界情况,如果当前枚举的背包容量装不下当前物品的时候,那么此时dp[i][j] = dp[i - 1][j],如果没有这个边界,那么数组就会越界访问下标-1。

i/j012345678910
000000000000
100000000003
200044444444
3000466610101010
4000467710111313
public int solution(int n, int C, int[] w, int[] v) {
    int[][] dp = new int[n + 1][C + 1];
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= C; ++j) {
            if (j >= w[i]) {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
            } else {
                dp[i][j] = dp[i - 1][j];
            }
        }
    }
    return dp[n][C];
}

这是最常规的解法,但是空间复杂度还可以再优化,因为通过看dp[i][j]的赋值,每次都只和dp[i - 1][…]有关,也就是只和上一行的状态有关。斐波那契问题中,发现当前状态只和前两步有关,空间上可以优化为只用两个变量来存储。现在发现只和上一行有关,那么只用存一行的状态就可以了,不用(n + 1)行来存储:

public int solution(int n, int C, int[] w, int[] v) {
    int[] dp = new int[C + 1];
    for (int i = 1; i <= n; ++i) {
        for (int j = C; j >= w[i]; -j) {
            dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
        }
    }
    return dp[C];
}

这里先不管内层循环为什么是倒序的,先看为什么内层循环不用枚举到1,而是到w[i]就停了,这和上面的讨论其实是一样的:

  • j >= w[i]表示当前容量可以装下当前的物品,那还是分为装和不装当前物品,dp[j]就表示不装,还是“上一层”的状态,dp[j - w[i]] + v[i]表示装当前物品。
  • 如果装不下,dp[j]还是“上一层”的状态,不会改变,所以就不需要进内层循环了。而之前用二维数组的时候,需要自己手动更新dp[i][j] = dp[i - 1][j],因为数组的初始值都是0,不进内层循环更新的话,没办法保持是“上一层”的状态。

比如当枚举到第3件物品(体积4,价值6),背包容量为3的时候,装不下体积是4的物品,那么需要保持上一层的状态,如果是一维数组,那么你只要不改它的值,就是上一层的状态,二维数组就需要在内层循环手动更新。

下面看最核心的问题,为什么内层循环是倒序的,一定要搞清楚,因为完全背包里内层循环是正序的。先说结论:因为倒序可以保证物品只被拿一次,正序表示物品可以拿多次

还是用上面的例子,那么dp[]初始状态全是0,现在枚举第一个物品(体积10,价值3),正序倒序结果都一样,dp数组变为:

012345678910
00000000003

然后枚举第二个物品(体积3,价值4),如果用正序进内层循环for (int j = w[i]; j <= C; ++j)

012345678910
000400800124

这显然是不对的,因为枚举第二件物品的时候,顶破天了价值只能是4,如果用倒序

012345678910
00044444444

其实这里最主要的问题还是在于一维数组,我们在能装下当前物品的情况下进行更新的时候,用的式子是dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]),用到的都应该是“上一层”的状态:

  • 如果我们是正序遍历,那么dp[j - w[i]]的值很可能已经被更新成当前层的状态了,也就是已经装了当前物品一次了,此时再 + v[i]就表示又装了一次。
  • 如果是逆序遍历,那么dp[j - w[i]]肯定还是上一层的状态,没有被更新,也就是当前物品还没被装,也就是逆序的时候,看到的一直是当前物品没有被装入的状态

如果觉得还是不太清楚,认为上面的例子0太多,可以再举一个简单的例子:第一个物品体积是1,价值是2,第二个物品体积是2,价值是3,背包容量是5:

枚举第一个物品,正序:

012345
0246810

会发现一直在装,但是逆序的话,看到的都是上一层状态(初始值)0:

012345
022222

然后枚举第二个物品,逆序:

012345
023555

完全背包

相比01背包,变的条件只有每件物品的个数都是无限的,说是无限,其实总重量也不能超过背包容量。还是先考虑二维数组的情况,需要改的地方很少,只需要表示装了物品i之后,还可以接着装物品i即可,这在上面讨论的时候已经有思路了,只要不更新上一层状态,更新当前层状态就可以了:

public int solution(int n, int C, int[] w, int[] v) {
    int[][] dp = new int[n + 1][C + 1];
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= C; ++j) {
            if (j >= w[i]) {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - w[i]] + v[i]);
            } else {
                dp[i][j] = dp[i - 1][j];
            }
        }
    }
    return dp[n][C];
}

这里j >= w[i],可以装下的时候,表示装入的动作用了dp[i][j - wj[i]] + v[i],而不是i - 1,因为i - 1表示的是上一层的状态,那么i永远是未装入的状态,i表示当前层,可能被更新过,也就是装入过,这时再更新就多装了一次。

那么一维数组的就更好写了,只用把倒序改为正序,可以多次装就行了:

public int solution(int n, int C, int[] w, int[] v) {
    int[] dp = new int[C + 1];
    for (int i = 1; i <= n; ++i) {
        for (int j = w[i]; j <= C; -j) {
            dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
        }
    }
    return dp[C];
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值