美文网首页
动态规划1.1--背包问题之0-1背包

动态规划1.1--背包问题之0-1背包

作者: rensgf | 来源:发表于2021-04-19 22:31 被阅读0次

    背包问题可以分为以上几种,在本章将从两个热点方面进行介绍:

    • 0-1背包及优化

    「力扣」第 416 题:分割等和子集(中等);
    「力扣」第 474 题:一和零(中等);
    「力扣」第 494 题:目标和(中等);
    「力扣」第 879 题:盈利计划(困难);

    • 完全背包

    「力扣」第 322 题:零钱兑换(中等);
    「力扣」第 518 题:零钱兑换 II(中等);
    「力扣」第 1449 题:数位成本和为目标值的最大数字(困难)。

    一、0-1背包

    1、问题定义

    有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。

    定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。设第 i 件物品体积为w[i],价值为 v[i],根据第 i 件物品是否添加到背包中,可以分两种情况讨论:

    • 第 i 件物品没添加到背包,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的前 i-1 件物品的最大价值,dp[i][j] = dp[i-1][j]
    • 第 i 件物品添加到背包中,dp[i][j] = dp[i-1][j-w[i]] + v[i]

    第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。因此,0-1 背包的状态转移方程为:
    dp[i][j] =max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]);

    2、问题分析

    我们按照之前的解题思路五部曲进行分析:

    (1)确定dp数组以及下标含义

    i表示前i件物品,j表示背包容量。dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。

    (2)确定状态转移方程

    ①装第i件物品,那么前i-1物品最大重量为j-w[i],最大价值为dp[i-1][j-w[i]];装入物品i后,最大价值为dp[i][j] = dp[i-1][j-w[i]] + v[i]
    ②不装第i件物品,最大的价值就是容量为j的背包装前i-1件物品,dp[i][j] = dp[i-1][j]

    (3)dp数组的初始化

    ①如果容量j=0,那么背包将装不了任何物品,所以dp[...][0]=0;
    ②装前0件物品,即没有物品时,容量再大也没有价值,dp[0][...]=0;

    (4)确定遍历的顺序

    可以看出,有两个遍历的维度:物品与背包重量
    那么问题来了,先遍历 物品还是先遍历背包重量呢?
    其实都可以!!但是先遍历物品更好理解。

    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]; // 这个是为了展现dp数组里元素的变化
            else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
        }
    }
    

    为什么也是可以的呢?

    要理解递归的本质和递推的方向

    dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 递归公式中可以看出dp[i][j]是靠dp[i-1][j]和dp[i - 1][j - weight[i]]推导出来的。

    dp[i-1][j]和dp[i - 1][j - weight[i]] 都在dp[i][j]的左上角方向(包括正左和正上两个方向),那么先遍历物品,再遍历背包的过程如图所示:


    再来看看先遍历背包,再遍历物品呢,如图:

    大家可以看出,虽然两个for循环遍历的次序不同,但是dp[i][j]所需要的数据就是左上角,根本不影响dp[i][j]公式的推导!

    但先遍历物品再遍历背包这个顺序更好理解。

    其实背包问题里,两个for循环的先后循序是非常有讲究的,理解遍历顺序其实比理解推导公式难多了

    (5)举例检验

    建议大家此时自己在纸上推导一遍,看看dp数组里每一个数值是不是这样的。

    做动态规划的题目,最好的过程就是自己在纸上举一个例子把对应的dp数组的数值推导一下,然后在动手写代码!

    3、空间优化--滚动数组

    在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
    观察状态转移方程可以知道,前 i 件物品的状态仅与前 i-1 件物品的状态有关,其实可以把dp[i - 1]这一层拷贝到dp[i]上,只用一个一维数组dp[j](一维数组,也可以理解是一个滚动数组)。
    这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。
    我们依然使用五部曲进行分析:

    (1)确定dp数组以及下标含义

    j表示背包容量。dp[j] 表示容量为j的背包,价值总和最大是多少。

    (2)确定状态转移方程

    ①装第i件物品,那么前i-1物品最大重量为j-w[i],最大价值为dp[j-w[i]];装入物品i后,最大价值为dp[j] = dp[j-w[i]] + v[i]
    ②不装第i件物品,最大的价值就是容量为j的背包装前i-1件物品,dp[j] = dp[j]
    此时,dp[j]=max(dp[j],dp[j-w]+v)

    (3)dp数组的初始化

    如果容量j=0,那么背包将装不了任何物品,所以dp[0]=0;

    (4)确定遍历的顺序

    这时候,遍历的顺序就很关键了。因为没更新的dp[j]保留了上一层的结果,也就是 dp[i-1][j]的结果,所以,在更新dp[j]时,要用到之前的dp[j-w],它必须是没被更新过的!
    当j从小到大遍历时,前面的dp[j-w]不断被更新,导致dp[j]在更新时,用的是已经更新过的dp[j-w],导致同一物体被放进去多次。
    因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w],防止将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解。

    (5)举例检验

      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]);
            }
        }
    

    相关文章

      网友评论

          本文标题:动态规划1.1--背包问题之0-1背包

          本文链接:https://www.haomeiwen.com/subject/fglilltx.html