【动态规划】绝X不翻车的01背包问题(详)


在动态规划中有这么一种背包问题,比较困扰,听一次会一次,过几天又忘一次,因此写一篇博客来详细的说明一下01背包问题,听懂不翻车!

在这里插入图片描述

领扣链接

题目:
n 个物品和一个大小为 m 的背包. 给定数组 A 表示每个物品的大小和数组 V 表示每个物品的价值。问最多能装入背包的总价值是多大?

注:

  1. A[i], V[i], n, m 均为整数
  2. 你不能将物品进行切分
  3. 你所挑选的要装入背包的物品的总大小不能超过 m
  4. 每个物品只能取一次
  5. m <= 1000并且len(A),len(V)<=100

思路分析:

拿到这道题目时,以一般人的思维考虑,想要使得背包中的总价值最大,就需要同时考虑n个物品的体积和价值,经过一番抉择后,得到最优组合解。

n件物品每一件都有放入或者不放入的可能性。

首先就需要考虑背包的体积和即将要放入的物品之间的大小关系。

  1. 某物品肥的要死,整的比背包还大,那么该物品就不存在被放入的可能性,丢了~
  2. 某物品的体积比背包小,那么就需要考虑是将它放入背包所产生的价值大还是不放入产生的价值大,选择这两选择中价值更大的。

将上面的思路进行转换成动态规划的四个角度就应该是:

  1. 状态定义F(i,j)前i个物品放进大小为j的背包中所获得的最大的价值量

    例如F(4,8)就代表着背包大小为8时考虑前4个物品放入的情况,使得背包中价值最大的最佳组合产生的价值量

  2. 状态间的转移方程定义F(i,j):

  • A[i-1] > j时,F(i,j)= F(i-1,j);代表着第i个物品的体积比背包还大,将之丢弃,此时背包的价值量和将前i-1个物品放进大小为j的背包所产生的最大价值量是一样的。

  • A[i-1] <= j时,F(i,j)= Max(F(i-1,j),F(i-1,j-A[i-1])+V[i-1])

    代表着第i个物品的体积比背包小,那么就存在放不放入背包的选择,F(i,j)的值将是这两种选择中能产生最大价值的那一选择。

    倘使不放入背包,那么此时背包的价值量和将前i-1个物品放进大小为j的背包所产生的最大价值量是一样的;

    倘使放进背包,那么就需要在已经对前i-1个物品进行最佳选择的背包中腾出第i个物品的体积(A[i-1]),这块儿体积就是用来迎接物品i的到来的,然后在加上第i个物品的价值(V[i-1])。

  1. 状态的初始化 F(0,j)= F(i,0)= 0;当没有放任何物品时或者背包的大小为0时,背包的价值都为0

  2. 返回结果F(n,m)即将n个物品放入大小为m背包产生的最大价值量

举例分析:

这里有4个物品,背包的大小为8,物品大小数组A[4]={3,5,1,4}; 物品价值数组V[4]={1,3,2,3};

为了更加形象的说明该问题,采取列二维表的方式来验证我们之前的分析思路:

在这里插入图片描述

横坐标为背包的容量(j),纵坐标为物品的编号(i),每个坐标就代表着前i个物品放进大小为j的背包中所获得的最大的价值量。

当没有物品或者背包的容量为0时,背包中的价值为0,此乃状态初始化。

当决定放1号是否放进背包时:

  • 背包容量小于3(1号物品大小)时,1号物品可以没有放下的可能,弃之~此时背包价值量为0

  • 背包容量大于等于3时

    • F(1,3) = max(F(0,3),F(0,3-3)+ V[0]) = F(0,0)+ 1= 1

    • F(1,4)= max(F(0,4),F(0,4-3)+ V[0]) = F(0,1)+ 1 = 1

    • F(1,8) = max(F(0,8),F(0,8-3)+ V[0]) = F(0,5)+ 1= 1

当决定放2号是否放进背包时:

  • 背包容量小于5(2号物品大小)时,2号物品可以没有放下的可能,弃之~,此时背包价值量和F(1,j)时相同(1<=j<=4)
  • 背包容量大于等于5时
    • F(2,5) = max(F(1,5),F(1,5-5)+ V[1]) = F(1,0)+ 3= 3
    • F(2,6) = max(F(1,6),F(1,6-5)+ V[1]) = F(1,1)+ 3= 3
    • F(2,7) = max(F(1,7),F(1,7-5)+ V[1]) = F(1,2)+ 3= 3
    • F(2,8) = max(F(1,8),F(1,8-5)+ V[1]) = F(1,3)+ 3= 4

当决定放3号是否放进背包时:

  • 背包容量小于1(3号物品大小)时,3号物品可以没有放下的可能,弃之~,此时背包价值量为0

  • 背包容量大于等于1时

    • F(3,1) = max(F(2,1),F(2,1-1)+ V[2]) = F(2,0)+ 2= 2

    • F(3,2) = max(F(2,2),F(2,2-1)+ V[2]) = F(2,1)+ 2= 2

    • F(3,8) = max(F(2,8),F(2,8-1)+ V[2]) = F(2,7)+ 2= 5

当决定放4号是否放进背包时:

  • 背包容量小于4(4号物品大小)时,4号物品可以没有放下的可能,弃之~,此时背包价值量和F(3,j)时相同(1<=j<=3)

  • 背包容量大于等于4时

    • F(4,4) = max(F(3,4),F(3,4-4)+ V[3]) = F(3,4)= 4

    • F(4,5) = max(F(3,5),F(3,5-4)+ V[3]) = F(3,1)+ 3= 5

    • F(4,8) = max(F(3,8),F(3,8-4)+ V[3]) = F(3,4)+ 3= 6

回溯归纳:

从表中的右下角开始回溯,当前i个物品产生的最大价值量和前i-1个物品产生的最大价值量相等,说明第i个物品没有被放入背包,反之,被放入背包。

在本例子中

F(4,8)!= F(3,8)说明物品4放进背包中,物品4的大小为4,说明背包中剩余4容量放其他物品

F(3,4)!= F(2,4)说明物品3放进背包中,物品3的大小为1,说明背包中剩余3容量放其他物品

F(2,3)== F(1,3)说明物品2没放进背包中,背包中剩余3容量放其他物品

F(1,3)!= F(0,3)说明物品1放进背包中,物品1的大小为3,背包容量为0

总结:物品1、物品3、物品4被放进背包中

代码实现

public class Solution {
    public int backPackII(int m, int[] A, int[] V) {
        int n = A.length;//物品的数量
        if(n == 0 && m == 0) return 0;//若没有物品或背包容量为0,就直接返回0,背包价值为0
        int[][] maxV = new int[n+1][m+1];//创建二维数组来存放价值状态
        //状态初始化
        //在Java中数组被初始化大小后,每个元素的大小默认为0,因此maxV[i][0]和maxV[0][j]不再初始化也是可以的
        for(int i = 0;i <= n;i ++) {
            maxV[i][0] = 0;
        }
        for(int j = 0;j <= m;j ++) {
            maxV[0][j] = 0;
        }
        //状态转移
        for(int i = 1;i <= n;i ++) {
            for(int j = 1;j <= m;j ++) {
                if(A[i-1] <= j) {
                    maxV[i][j] = Math.max(maxV[i-1][j],maxV[i-1][j-A[i-1]]+V[i-1]);//背包容量大于物品i的情况
                }else{
                    maxV[i][j] = maxV[i-1][j];//背包容量小于物品i的情况
                }
            }
        }
        return maxV[n][m];//返回结果
    }
}

代码升级

事实上,动态规划问题中的状态转移是指转移一步的所获得的状态,在本题中,也就是说第i个物品放入情况所获得的背包最大价值只会和第i-1个物品放入情况所获得的背包最大价值有关,比那更之前的物品放不放,背包价值如何根本就不需要关心,因为动态规划问题讲究的就是记录每个小问题的状态,然后一步步推导大问题的过程。

既然如此,我们可以干脆就建立一个大小为m(背包最大容量)一维数组来存放第i-1个物品放入与否后背包的价值得了,第i个物品放入与否后背包的价值直接在那一维数组中改动。

思想和上面是一样的,不同的是数组变成了一维的。而且在第二层遍历的时候要从后向前遍历,如果从前向后遍历的话,就会出现后面的状态想要用到前面的状态的值时,前面状态值早就被篡改了,所得到的结果自然会出错。

关于从后向前遍历这一点可以举个简单的例子:

array[5] = {1,2,3,4,5};

现在想让array[0]不变,数组中其他元素都变成其前面一个元素的2倍,即array[i] = array[i-1]*2 (0<i<5)

方法一:重新创建一个数组a[]来存放结果,那么遍历array时从前往后还是从后往前都无所谓,array数组没有被篡改,结果都一样正确

方法二:为节省空间,干脆直接在array数组上直接改得了,如果从前向后,array[0]= 1,array[1]=2,array[2]= 4,array[3]= 8 !?出现BUG了,不应该是6蛮,原因就在于后面的结果会用到array数组前面的值,从前向后遍历就会篡改array数组前面的值,从后向前就不会出现这样的状况。

方法一就相当于本案例中创建二维数组的情况,方法二就相当于本案例中创建一维数组的情况,因此需要注意遍历方向问题,至关重要!

public class Solution {
    public int backPackII(int m, int[] A, int[] V) {
        int n = A.length;//物品的数量
        if(n == 0 && m == 0) return 0;//若没有物品或背包容量为0,就直接返回0,背包价值为0
        int[] maxV = new int[m+1];//创建一维数组来存放价值状态
        for(int j = 0;j <= m;j ++) {
            maxV[j] = 0;
        }
        for(int i = 1;i <= n;i ++) {
            for(int j = m;j >=0;j --) {
                if(A[i-1] <= j) {
                    maxV[j] = Math.max(maxV[j],maxV[j-A[i-1]]+V[i-1]);
                }
            }
        }
        return maxV[m];
    }
}

完!

评论 35
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

富春山居_ZYY(已黑化)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值