保姆级01最大背包问题讲解

学习动态规划,始终绕不开01最大背包问题。相较于求解斐波那契数列,01最大背包问题是更标准的动态规划题目(因为它目标是求最值)。接下来我们一点点剖析01最大背包问题的每个细节,去掉那些人云亦云的部分,重点突出一些容易让人迷惑的地方,力求小白也能看明白。

问题描述

有一个背包,容量 capacity=5,有 3个物品,物品不可分割。每件物品和价值如下,要求尽可能让装入背包中的物品总价值最大,但不能超过总容量。

物品编号123
重量134
价值152036

我们用weights表示物品重量数组,用values表示物品价值数组。

即weights = [1, 3, 4],values = [15, 20, 36]

问题分析

大部分人很容易得出这是一个动态规划问题。但若要问为什么,恐怕很少人能够说清楚。事实上,它是动态规划问题的原因很简单:因为它符合动态规划问题的特征。那么,动态规划问题有哪些特征呢?我们来看下:

  • 求最值。这个从题目中一眼就可以看出来,我们之所以说斐波那契数列问题不是严格意义上的动归问题,也是因为它并不涉及求最值。

  • 最优子结构(字都认识系列)。翻译成人话就是:大规模问题的解可以由小规模问题的解推导出来。对应到实际案例,具体有:

    • 求解斐波那契数列的第n位,可以由第n-1位以及n-2位的数字推导出来;

    • 求背包容量为c,物品个数为n时的最大价值,可以由背包容量为c’​(c’<c)​,物品个数为​n’(n’<n)​时的最大价值推导出来;

    • 求序列长度为n的最长递增子序列,可以由序列长度为n’(n’<n)​的最长递增子序列推导出来;

关于第2点,我们在后面代码部分会更深刻地体会到,为什么最优子结构是动态规划问题的必要条件。

求解套路

当我们一旦确定是一道动归题目的时候,就可以直接套以下框架来思考,这些是着手coding前必要的准备过程,这个过程的终极目标是:找到状态转移方程,翻译成人话就是:找到大规模问题和小规模问题的关系(类似于斐波那契数列中的 f ( n ) = f ( n − 1 ) + f ( n − 1 ) f(n)=f(n-1)+f(n-1) f(n)=f(n1)+f(n1))。步骤如下:

  • 明确有哪些状态。翻译成人话:就是有哪些变量。再翻译翻译,就是在**小规模问题向大规模问题转变时,哪些量会发生改变。**具体到01最大背包问题:【最大价值】不消多讲,求的就是它,它肯定是其中的一个变量。除了它,还有【物品个数】以及【背包容量】,【最大价值】是我们欲求解的值,不妨将其称为【因变量】,后面的两个变量称为【自变量】。

  • 明确dp数组的含义。我们都知道求解动归问题要设一个dp数组(不知道也无妨,往后看自然会明白这是啥)。事实上,设dp数组,其实就相当于初中解应用题时设的方程。为了更好地理解,我们不妨回顾一道经典高中题目(好吧,我编的😂):

    有一堆砖,

    • 小王先搬3天,剩下的小李搬,小李6天可以搬完;

    • 小王先搬2天,剩下的小李搬,小李8天可以搬完;

    • 小王先搬1天,剩下的小李搬,小李搬6天后,还剩10块砖;

    求砖的数量。

    我们拿到这道题的时候,可以在1s内设出变量,并潇洒写出方程组:

    { z = 3 x + 6 y z = 2 x + 8 y z = 2 x + 6 y + 10 \begin{cases} z=3x+6y\\ z=2x+8y\\z=2x+6y+10\\ \end{cases} z=3x+6yz=2x+8yz=2x+6y+10

    其中,z表示砖的数量,x表示小王每天可以搬多少砖,y表示小李每天可以搬多少砖。

    我们来仔细回顾下心理路程:拿到这道题,之所能直接想到将砖的数量设为z,是因为它是我们的求解目标,即【因变量】(这也是数学老师常常强调的,求什么设什么),而想到设置x和y,是因为x和y是【自变量】。

    抛开x、y、z本身的意义,单看 z = 3 x + 6 y z=3x+6y z=3x+6y,只要x和y发生变化,z是不是就随之发生了变化呢?是不是跟01最大背包对应上了?对应的,背包的容量和物品的个数是制约背包价值的两个因素。

    好,我们回归正题,dp数组到底怎么设?一言以蔽之,dp的意义一般设为要求解的量(如最大价值、最长子序列等等),而dp的维度则要视状态即【自变量】的个数而定,对于当前问题,由于有两个状态,背包容量&物品个数,因此dp设为二维的,具体哪一维对应哪个自变量,怎么方便怎么来,只要你开心怎样都可以。

    铺垫这么多,我们终于可以得到,dp数组应该设为:

    dp[i][j]

    其意义是面对前i件物品,背包容量为j时(此处一定注意,j表示的是背包容量,而不是背包剩余的容量!!!),可以获得的最大价值。例如dp[2][3]的意思是,面对前2个物品背包容量为3时(并不是容量剩下3),可以获得的最大价值。

  • 初始化 dp[i][j]。此步骤虽然简单,但尤其关键。需要注意两个点:

    • 每一维的长度。dp应该的每一维应该多大,即dp应该是几×几的?这里直接给出结论:应该初始化为【自变量1的个数+1】×【自变量2的个数+1】。为什么要+1,我们在coding时会有深刻体会。
    • 初始化的值。此处也直接给出结论:与问题反着来就行,求最大值,就初始化为一个最小值(相对最小,例如该题价值不可能为负数,初始化为0即可);求最小值,则初始化为一个最大值。
  • 状态转移方程,翻译成人话就是:小规模问题向大规模问题转化的关系表达式,是最难的一步,也是我们的终极目标。有一点需要明确的是,当我们求解dp[i][j]时,它以前的值已全部已知,即dp[i’][j’] ( ( (i’​<i,​j’​<j)是已知的。不同的题目的状态转移方程千变万化,很难有规律可循,但是有技巧:

    • 做选择。动归题目很多是做选择题。如01最大背包问题中,面对当前物品,你可以选择装或不装进背包。最长递增子序列问题中,面对当前字符串,你可以选择将它作为递增序列中的一员,反之也可以。

    • 分情况讨论,找出选择前后的递推关系。如前所述,很多时候我们需要做出选择,而一旦做出不同选择,就会产生不同的后果,因此一般需要分情况讨论。对于01最大背包问题来讲,面临当前物品i,背包容量为j时:

      • 装得下(包容量比物品重量大)。此时又有两种选择:

        • 装进包里。则此时的价值为: d p [ i ] [ j ] = d p [ i − 1 ] [ j − w e i g h t s [ i ] ] + v a l u e s [ i ] dp[i][j]=dp[i-1][j-weights[i]]+values[i] dp[i][j]=dp[i1][jweights[i]]+values[i]请注意,网上对此公式的解释很多是错误的,j-weights[i]的意义并不是装了物品i,因此背包容量减去对应重量!,而是先腾出装i的空间,即将数组dp回退到dp[i-1][j-weights[i]],再把物品i装进包里,相应地,价值也要加上values[i]。
        • 不装进去。就简单了,此时包里的东西没变,总价值也没变,与面临前两件物品可以获取的最大价值相同,故dp[i][j]=dp[i-1][j]。

        这两种情况到底选哪个呢?直觉上觉得总比不装获得的最大价值要大吧?但是请注意,我们的前提是,要先腾出装i的空间,这也意味着有可能需要剔除掉一个或多个物品,所以谁大谁小还不一定呢,这也是我们要取max的原因(这一点很容易令人迷惑)。总结一下,如果能装的下,则有:

        d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] , d p [ i ] [ j ] = d p [ i − 1 ] [ j ] ) dp[i][j]=max(dp[i-1][j-weight[i]]+value[i],dp[i][j]=dp[i-1][j]) dp[i][j]=max(dp[i1][jweight[i]]+value[i],dp[i][j]=dp[i1][j])

      • 装不下。与不装进去一样, d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j]=dp[i-1][j] dp[i][j]=dp[i1][j]
        至此,我们的求解套路完毕。为了更深刻地理解选择的过程,不妨将整个选择过程(也就是dp数组)用表格记录下来,此处我们选择i=3,j=4来考察,因为该处产生了突变,更方便我们深刻理解算法原理。

avatar

正如前面所述,初始化dp每个维度长度都**+1**,i=3,j=4,而4 >=weights[3],即背包容量为4时可以装的下物品3,具体选择过程如下:

  1. 假设选择不装物品3,则可获取的最大价值和物品为2时可获得的最大价值相等,即dp[2][4]( dp[i-1][j] )。

  2. 假设选择装物品3,需要将背包腾出装3的空间,发现背包容量和物品3容量相等,这也意味着要想装物品3,需要将背包其他物品都倒出来,我们称其为回退。

  3. 回退后对应的可获取的最大价值为dp[2][0]+values[3] (dp[i-1][j-weights[i]]+values[i])。

  4. 比较两种选择哪个更大。

    d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] , d p [ i ] [ j ] = d p [ i − 1 ] [ j ] ) dp[i][j]=max(dp[i-1][j-weight[i]]+value[i],dp[i][j]=dp[i-1][j]) dp[i][j]=max(dp[i1][jweight[i]]+value[i],dp[i][j]=dp[i1][j])

    35<36,故dp[3][4]=36。

解题框架到此结束,具体代码请移步公众号,将会着重讲解令人疑惑的以及易错部分。
在这里插入图片描述

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值