leetcode:0-1背包问题

力扣0-1背包问题

1-1: 问题描述

有N件物品和⼀个最多能被重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能⽤⼀次,求解将哪些物品装⼊背包⾥物品价值总和最⼤。
有点抽象,换一个具体的场景来描述:
题目描述:总重量 total weight7,共有4个物品,分别是:

weightvalue
11
34
45
57

问怎么才能使得背包的总value最大,每个物品只能放一次。 不知道大家是否想过为什么叫0-1背包呢,所谓的0-1意思就是对于每一个物品我们只有放入和不放入两种选择。

1-2: 确定状态

dp[i][j] 代表从[0, i]中选物品放入到重量为j的背包中,背包的价值最大是多少。其中,[0, i]是闭区间的,就是说既包含0,又包含i。把物品从0开始编号,这边一共4个物品,i的取值是[0, 3]。要清楚,j是代表背包的重量。i是代表选择物品的情况。

1-3: 确定转移方程

对于任意一个物品有两种状态,放入和不放入。

  • 放入:剩余重量=当前包的重量-放入的该物品重量,拿着剩余重量,从剩余物品中选取来尽可能使总价值最大化后,再加上本物品的价值。也就是,放入的最大总价值=剩余重量的最大价值+本物品的最大价值。
  • 不放入:剩余重量=当前包的重量,拿着剩余重量,从剩余物品中选取来尽可能使总价值最大化。不放入的最大总价值=剩余重量的最大值。

这边:拿着剩余重量,从剩余物品中选取来尽可能使总价值最大化。其实就是这里的子问题,动规dp的精髓就是,记录下子问题的最优解。
从而可以得到dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])
dp[i-1][j]就是指,这个物品不放入的情况,剩余重量不变还是j,剩余物品,i-1很好理解,我这个不选,那么可选的剩余物品就是[0, i-1]。在这种情况下,获得的总价值,就是dp[i-1][j]填入的值。
dp[i-1][j-weight[i]] + value[i]就是指,那么就是从剩余物品中[0,i-1]中去选择,然后放入[j-weight]重量的背包中,使其总价值最大化,再加上我放入背包中的物品i的价值。

1-4: 不急着初始化,先看一个例子,模拟下整个过程

继续强调,i是物品的序号,j是背包剩余的重量。

  • i=0,对于重量为1的的0号物品,当背包中的重量分别为[0,7]的时候,从1开始,装入背包,总价值都只能是1,可以填入下面的表格,这里不用考虑,不放入的情况,因为i的顺序是从0到3,不放入0号物品,就等于没有价值了。
    在这里插入图片描述

  • i=1,对于重量为3的{1号}物品,此时考虑从{0号,1号}[0,1]这两个物品中选择,进行装入背包。

    • 重量j=0,很明显,都装不了,填入0。
    • 重量j=1,明显这边重量不够装入3号,所以等于现在重量是1,{0号,1号}两个物品中,剔除了1号,还剩{0号},于是问题转变为子问题->从{0号}中选择物品,填入j=1的背包中,也就是dp[0][1]的值,也就是继承了这个值。

在这里插入图片描述

  • j=2,装不下1号,所以同上,继承上面的1

  • j=3,有变化,j=3时,是可以装入1号物品的,所以有两种情况分别是装入1号和不装入1号。注意,我们表格里面都是填写的各种情况的最优解。

    • 装入1号物品,剩余重量=3-3=0,问题变成子问题:从{0号}(1号已经装入了,剩余物品就只有0号了)物品中,选择物品装入重量为0的背包中。很明显,子问题的最优解就是dp[i-1][j-weight3]=dp[0][0]。所以装入1号物品的总价值为dp[i-1][j-weight3]+value[1]=dp[0][0]+4=4
    • 不装入1号物品,剩余重量=3,问题变为:从{0号}物品中,选择物品装入重量为3的背包中。子问题的最优解就是dp[0][3]=1的值。

    由上面两种情况,我们取Math.max()=4就可以得到将{0号,1号},装入重量为3的背包的最优解。

  • j=4,5,6,7依旧按照上面的思路,去填写值。这边再写一个j=7的时候

    • 装入1号物品,剩余重量=7-3=4,问题变成:从{0号}(1号已经装入了,剩余物品就只有0号了)物品中,选择物品装入重量为4的背包中。也就是dp[0][4]的值,所以这种情况的总价值=dp[0][4]+vale[1] = 1+4 = 5
    • 不装入1号物品,剩余重量=7,从{0号}物品中,选择物品装入重量为3的背包中。子问题的最优解就是dp[0][4]=1的值。
      取Math.max()=5,填表。

在这里插入图片描述

  • i=2,i=3时,按照上面的思路,依次填写表格。最后得到结果:

在这里插入图片描述

1-5: 初始化

由上面的一个例子,我们很容易发现,一个值会依赖上面一行,左上方的值,所以初始化的时候,应该将这两行进行初始化。如表:当然从状态转移方程也可以看出值之间的依赖关系。

在这里插入图片描述

1-6: 确定遍历顺序

很明显从左到右,从上到下的顺序。先遍物品好理解一点。

1-7:二维数组 代码

public static int bag(int[] weights, int[] value, int bagWeight) {
    int n = weights.length;
    int m = bagWeight;

    // 物品编号:0~n-1 背包重量0~m
    int[][] dp = new int[n][m + 1];

    // initialization,初始化0的过程java可以省略,因为int数组默认初始化为0,二维数组中间部分也可以不手动初始化,因为自动初始化为0
    // 并且所有的值后面会改动
    for (int j = 0; j < m + 1; j++) {
        if (j >= weights[0]) {
            dp[0][j] = value[0];
        } else {
            dp[0][j] = 0;
        }
    }

    for (int i = 0; i < n; i++) {
        dp[i][0] = 0;
    }

    for (int i = 1; i < n; i++) {
        for (int j = 1; j < m + 1; j++) {
            // 放入i号物品的时候
            if (j >= weights[i]) {
                dp[i][j] = Math.max(dp[i - 1][j - weights[i]] + value[i], dp[i - 1][j]);
            } else {
                // 不放入i号物品
                dp[i][j] = dp[i - 1][j];
            }
        }
    }
    System.out.println(Arrays.deepToString(dp));
    return dp[n - 1][m];
}

    public static void main(String[] args) {
        int[] weights = new int[]{1, 3, 4, 5};
        int[] value = new int[]{1, 4, 5, 7};
        bag(weights, value, 7);
//        int[] weights = new int[]{1, 3, 4};
//        int[] value = new int[]{15, 20, 30};
//        bag(weights, value, 4);
    }

----output----
[[0, 1, 1, 1, 1, 1, 1, 1], [0, 1, 1, 4, 5, 5, 5, 5], [0, 1, 1, 4, 5, 6, 6, 9], [0, 1, 1, 4, 5, 7, 8, 9]]
  • 代码讲解:
    • 初始化,根据上面的分析中转移方程一般式中的值依赖关系,先对第一行和第一列初始化
    • 两层循环,放入的前提条件就是背包重量要够,所以这个判断条件也好理解。可以放入的时候,就回到了状态转移方程的两种情况。重量不够放入的时候,就只有一种情况,用现有的重量和之前的物品,得出最大值。也就是继承dp[i-1][j]的值。大白话讲:
      • 拿到一个袋子,我们首先看袋子的重量,这个重量能不能放进我现在的这个物品
      • 能放。能放,我不一定非要放呀。我可以选择不放。
        • 情况1:放进去的价值更高呢?情况2: 不放进去的价值更高呢?
      • 不能放。我都放不进去了,还考虑放进去的价值做啥呢?
        • 情况1:不放进去的价值。

1-8:一维数组 代码对空间复杂度的优化(滚动数组)

什么是滚动数组?在我看来可能是复用空间的意思。

滚动数组是DP中的一种编程思想。简单的理解就是让数组滚动起来,每次都使用固定的几个存储空间,来达到压缩,节省存储空间的作用。起到优化空间,主要应用在递推或动态规划中(如01背包问题)。因为DP题目是一个自底向上的扩展过程,我们常常需要用到的是连续的解,前面的解往往可以舍去。所以用滚动数组优化是很有效的。利用滚动数组的话在N很大的情况下可以达到压缩存储的作用

    /**
     * 对空间复杂度进行优化,由2维数组优化成1维数组
     */
    public static int bag2(int[] weights, int[] value, int bagWeight) {
        int n = weights.length;
        int m = bagWeight;

        int[] dp = new int[m + 1];

        // initialization
        for (int j = 0; j < m + 1; j++) {
            if (j > weights[0]) {
                dp[j] = value[0];
            }
        }
        for (int i = 0; i < n; i++) {
            for (int j = m; j >= 0; j--) {
                if (j >= weights[i]) {
                    dp[j] = Math.max(dp[j], dp[j - weights[i]] + value[i]);
                }
              	// 不大于的时候,继承原来的值,不用改。
            }
        }
        System.out.println(Arrays.toString(dp));
        return dp[m];
    }

为什么能这么优化,通过第一个案例,我们可以清楚发现,一个一般值依赖于自己上一行(头顶)简称x和上一行的左边部分简称y,也就是说,只和上一行有关系。

最终比较式是,value[i]+y>x?val[i]+y: x; 如果我们将数组换成一维,初始化之后,第二次遍历这个一维数组的时候,拿到的就是上一行的数据,读取值再修改,就变成了当前行的数据。那么为什么要从右向左遍历呢?因为二维数组当前行的数据,依赖于两个上一行的数据(x,y),先把左边的y改掉了,依赖于上一行的数据就丢失了。从右开始向左遍历,就可以保证保留了上一行的数据。可以动手试试,模拟一下就懂了。

看见一个很好的讲解视频,供大家学习。很好

https://www.bilibili.com/video/BV1C7411K79w?p=2

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

河海哥yyds

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

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

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

打赏作者

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

抵扣说明:

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

余额充值