c++ 多重背包状态转移方程_动态规划入门——详解经典问题零一背包

c90d52cf1bba1620b4f3843e1bc1e444.png

本文始发于个人公众号:TechFlow,原创不易,求个关注

今天是周三算法与数据结构专题的第12篇文章,动态规划之零一背包问题。

在之前的文章当中,我们一起探讨了二分、贪心、排序和搜索算法,今天我们来看另一个非常经典的算法——动态规划

在acm-icpc竞赛领域,动态规划是一个非常大的范畴,当中包含了许多变种,而且很多变种难度极大。比如在各种树上和图上以及其他数据结构上做动态规划,这会使得问题非常复杂。好在非竞赛选手并不需要了解到那么深入,一般来说,吃透背包九讲,就足够笑傲各种面试了。所以周三的算法专题我们开始全新的篇章——背包系列,今天和大家分享背包九讲中的第一讲,也是最简单的零一背包问题。

背包和零一背包

没有竞赛经验的同学在看到这个标题的时候可能会一头雾水,动态规划和背包有什么关系。其实没有关系,我也不是陈奕迅的粉丝,只是当初最经典的动态规划问题用背包做了题面,还引发出了各种变种。后来在教学的时候为了方便,于是沿用了前人的名称。

之前我们在怪盗基德偷宝石的问题当中提到过背包问题,其实很简单,就是说我们当下有一个容量是V的背包,和n个体积分别是v[i],价值是w[i]的五品。请问,在背包容量允许的前提下,我们最多能够获得多少价值的物品

由于每种物品只有一个,也就是物品只有拿和不拿两种状态,所以这个问题被称为零一背包问题

贪心与反例

这种问题我们最先想到的就是贪心法,比如优先拿价值大的物品,或者是性价比高的物品,但是我们很容易构思出反例。

举个例子,比如背包的容量是10,我们有3个物品,体积分别是6,5,5,价值是10,8,8。这个反例可以证明两种贪心策略都不生效,因为价值最大的是10,它的体积是6,我们一旦拿了它就没有空间再继续获取其他物品,而显然拿两个5的情况是最优的。同样,体积是6的物品也是性价比最高的,性价比优先的贪心策略同样不生效。

实际上不仅这两种贪心策略不生效,所有能够想到的贪心策略都不生效。这个问题看起来简单,但是并不是那么容易解决。实际上这个问题一直困扰着计算学家,直到上世纪六十年代,动态规划算法横空出世,完美地解决了这个问题。

动态规划

动态规划算法的英文是dynamic programming,算是很直白的翻译了。规划我们都很好理解,但是动态应该怎么理解呢?又怎么来动态地规划呢?关于这个问题的思考直接关系到算法的本质。

动态规划算法的本质是状态的记录和转移,我们结合刚才的问题,有没有想过为什么贪心算法不可行?其实很简单,因为我们没办法确定背包什么状态是完美的。虽然我们知道背包的容量是V,但是我们并不知道最优的情况下我们能装多少,最优的结束状态是什么。我们把空间V看成了一个状态来进行贪心,贪心得到的结果是最优的,但是只是贪心能达到的状态的最优解,并不是全局的最优解,因为背包容量的限制,很有可能我们贪心策略下无法达到真正最优的状态。

用刚才的例子解释一下上面这段话,在贪心算法下,我们会选取容量是6,价值是10的物品,这个物品拿取了之后背包的状态是6,获取的价值是10。这个状态是贪心能够达到的最终状态,对于这个状态而言,它是最优解,但是这个状态并不是整体最优的情况,因为在贪心策略下,无法达到容量10全用完的状态。

理解了这个问题之后,再去推导解法就顺其自然了。贪心策略可以获取一些状态最优的情况,那么我们能不能记录下所有状态能够达到的最优的情况,最后在这些最优的情况当中选取一个最优的,它不就是整体最优解了吗?

动态规划正是基于上述思路展开的,它解决的不是一个状态的最优解,而是所有状态的最优解。

状态与转移

看到这里,你肯定还没理解动态规划算法,但是应该已经有一些大概的感觉了。这是对的,有正确的感觉是正确认识的前提。我们循序渐进,再来看状态这个概念。

我们刚才提了这么多次,究竟状态是什么呢?这是一个比较抽象的概念,在不同的问题当中它有着不同的含义。在背包问题当中,状态指就的是背包容量的使用情况。由于背包问题中物品的体积是整数,显然背包容量的可能性是有限的,这点不起眼,但是很重要,如果状态不是整数,那么虽然存在动态规划的可能,但是代码实现可能比较麻烦。

明白了背包的容量是状态之后,我们可以进一步想明白,背包的容量是会变化的。变化的原因是因为我们往里面放了东西,放了东西之后,背包的状态会发生变化,会从一个状态转移到另一个状态。状态的转移中间伴随着我们放入东西,我们放的物品并不是固定的,而是有多种选择的,我们决定放入A而不是BC,这是一种决策,决策会带来状态的转移,不同的决策会带来不同的转移。

比如当前有一个背包,它的容量是10,我们在其中已经放入了一个体积是3,价值是7的物品。如果这个时候,我们经过选择再放入一个体积是4,价值是5的物品。那么显然,背包占用的容量会变成7,价值会变成12。这个过程就是一个经典的状态转移过程,这也是整个动态规划算法的核心。

基本的概念和思想已经介绍完了,接下来就是用这些概念来解决实际问题了。

最优状态

我们前文说了,动态规划最后会获取所有状态的最优解,再从中选取全局最优的。那么它是怎么获取局部最优解的呢?

在回答这个问题之前,我们先来思考两个问题。

首先,假如我们已经知道了背包体积是3时的最大价值是5,这时候我们决定放入一个体积是4,价值是5的物品,那么背包的体积会增加到7,那么这个时候获得的是体积6的最优解吗?

这个问题不难回答,我们稍微想想就知道,很有可能不是。举个最简单的例子,假如我们有一个体积是7,价值是20的物品。那么显然要比放这两个物品更优。虽然状态3最多能获得价值5,状态7也可以由状态3转移得到,但是这并不一定是最优的。也就是说最优的状态转移出去,并不一定也能得到其他状态的最优值

我们把问题反过来就不一样了,如果我们知道了体积6的最优解,并且还知道它是由体积等于4转移得到的,那么我们能不能确定体积4的状态也是对应的最优解?

这次的答案就变了,是正确的,因为如果体积4时还有更好的解法,那么体积6理应也会变得更好才对,这和我们的假设矛盾了。

我们总结一下上面的两个结论,也就是说局部最优的情况转移出去并不一定是最优的,但是局部最优一定也是由其他局部最优的状态转移得到的。这句话有点像绕口令,但是我觉得应该不难解释。就好比学霸去考试,不一定能考第一,但是考到第一的一定是学霸。局部最优就是学霸,转移就是考试。局部最优转移出去并不一定是转移之后状态的最优,有可能还有其他更好的转移策略,但是对于某个状态最优的情况而言,它一定也是从之前的某个最优状态转移得到的。

并且状态的转移也是有顺序的,比如在这题当中,背包当中放入了物品体积只可能增加,不可能减小,意味着状态只能从小的转移到大的。

我们捋一捋思路,已经很明确了,状态可以转移,状态的转移有顺序,局部最优一定是由其他局部最优转移得到的。由于我们并不知道当前的转移能否达到最优状态,所以我们需要用一个数组或者是容器来记录所有状态历史上曾经达到过的最值。最后从所有的最值当中再选出一个最值来,就是最后问题的解。

到这里,如果是一般的动态规划问题,已经解决了,但是零一背包还有一个细节需要考虑。

无后效性

我们先来看下整个的计算流程,首先我们需要从最初状态开始,这个最初的状态很好办,就是背包是空的时候,这时候的价值是0,体积也是0,这也是它的最优状态,这个很好理解,因为我们不能无中生有。

所以我们从0开始转移状态,状态转移伴随着决策,在这题当中体现在选取不同的物品上。我们遍历物品,作为决策,再遍历能够应用这些决策的状态,就拿到了所有的状态转移。最后,我们用一个容器记录一下所有状态转移过程当中达到的局部最优解,于是就结束了。

这个过程看起来非常正常,没有任何异常,但实际上,问题来了。

我们还用刚才的题面举例,背包容量是10,3个物品,体积分别是6,5,5,价值是10,8,9。我们从0开始拿取第三个物品,转移到了状态5,此时的价值是9。这个时候,我们继续往后遍历的话还会遍历到状态5,它已经是拿取了物品3,价值9的信息了。因为一个物品只能拿一次,所以我们不能再用物品3转移状态5,否则就违反了题意

你可能会说这个问题不难,我们可以在状态当中也记录之前做过的决策嘛,只要在决策的时候加一个判断就好了。

表上面看是因为物品不能重复拿的限制,实际上是因为我们的状态之间会有影响。也就是说我们前面做的决策很有可能影响后面的状态做决策,这种状态之间的前后影响称为后效性。显然在有后效性的场景下我们是不能使用动态规划算法的,并不是所有问题都可以通过加上判断解决,我们需要解决后效性这个本质问题,也就是说我们要想办法消除后效性。

在这个问题当中,这一点很容易做到。我们只需要控制一下状态和决策的遍历顺序,将之前的决策与之后的决策分开,使它们互不影响即可。如果我们先遍历状态,再遍历每个状态可以采取的措施,这样必然会造成前后影响。因为前面做了的决定,后面就不能再做。但是后面并不能感知前面究竟做了什么决定。所以比较好的方法是先遍历决策,再来遍历可以采取这个决策的状态。为了避免决策前后的互相影响,我们采取倒序的方式遍历状态

我们举个例子,假设背包容量还是10,我们枚举的第一个物品体积是3,价值是5。我们倒叙遍历状态7到0,因为对于大于7的状态而言,并不能采取这个决策(总体积会超)。因为对于大于7的状态而言,我们不能采取这个决策(总体积会超过限制),对于状态7而言,我们可以采取这个决策,转移到状态10。我们并不知道这样转移会不会达成最优,所以我们这样来记录:dp[10] = max(dp[10], dp[3] + 5).

我们接着遍历体积6,可以转移到状态9。

由于我们是倒序遍历,所以当我们用状态7更新状态10时,状态7本身并没有被这个决策更新过。即使后面我们在遍历到状态4时更新了状态7,也不会影响状态10的结果。因为是倒序遍历的,我们不会再用同一个策略更新到状态10了。如果是正序遍历,则无法避免。同样的物品,我们很有可能会出现,用状态1更新状态4,再用状态4更新状态7,再用状态7更新状态10的情况出现。而这种情况其实对应了使用了多个同样的物品,这就和题意矛盾了。

举个例子,假设有一个物品体积是2,它的价值是5。我们遍历状态0的时候,会更新状态2,我们遍历到了状态2,又用同样的物品更新了状态4,得到了10。那么对于状态4而言,它其实相当于拿了2个这个物品,也就是说被同一个决策更新了两次。但是我们的物品最多只有一个,显然就不对了。

5b7889754b85df58b901fd46460f46e7.png

动态规划当中因为无法判断当前枚举的状态的来源,所以不允许出现后效性,如果解决不了则不能使用动态规划。这也是动态规划最基本的原则,在这题当中,我们是巧妙变换了决策和状态枚举的过程,消除了后效性。在其他题目当中未必相同,我们需要根据实际情况进行判断。

如果你在做题思考的过程当中忘记了动态规划的前提,就想想零一背包当中拿取物品的情况。物品只有一个,只能拿一次。前面拿过了后面还能拿,就违反了后效性。

状态转移方程

我们整理一下刚才关于状态转移的思路,有以下几点:

  1. 我们从状态0开始,状态0的最优价值是0.
  2. 考虑后效性的问题,确保没有后效性
  3. 执行决策的时候,会发生状态转移,记录状态对应的最优解

在这个问题当中,决策就是获取物品,状态就是背包容量。由于拿取物品会引起背包容量变化,并且每个物品只有一个,为了避免产生后效性,我们需要先枚举决策,再枚举状态,保证每个决策只在每个状态上最多应用一次。在此过程当中,需要一直记录每个状态的最优解。

由于背包的容量是V,我们只需要用一个容量是V的数组就足够记录所有的状态。

dp

dp记录的是所有的状态,我们用max(dp[v+i.v], dp[v] + i.w)来更新dp[v+i.v]状态的值,由于当前的决策不一定比之前的更好,所以要加上max操作,保证每个状态记录下来的结果都是它最优的。当所有的状态的最优解都有了之后,显然整个问题的最优解也在其中了。

上面这个记录状态转移过程的式子叫做状态转移方程,它也是动态规划算法的核心概念。很多时候,在我们解动态规划问题的时候,会在草稿纸上推演状态转移方程。如果状态转移方程能清楚地列出来,距离写出代码也就不远了。

代码

上面的转移方程已经非常接近最后的代码了,真正写出来也就只有几行而已:

dp 

总结

关于零一背包的前后推导以及当中所有的概念始末就算是介绍完了,虽然我们用了这么多篇幅来介绍这个算法,但是真正写成代码也就只有短短几行。单从代码行数来看,动态规划可以说是实现代码最短的算法了,只是虽然它代码不长,但是思路并不简单,尤其是当中的下标以及循环的顺序等细节,希望大家不要掉以轻心。

今天零一背包的问题到这里就结束了,下周的算法专题我们继续背包问题,来看看01背包的进阶版——完全背包和多重背包问题,敬请期待。

如果觉得有所收获,请顺手点个关注或者转发吧,你们的举手之劳对我来说很重要。

ac3a155a86ebb4cb962c95af1d8a5f7a.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/374436.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python定义一个圆_Python-矩形和圆形

原博文 2019-11-11 12:34 − Exercise 15.1. 定义一个叫做Circle 类,类的属性是圆心 (center) 和半径 (radius) , 其中,圆心 (center) 是一个 Point 类,而半径 (radius) 是一个数字。 实例化一个圆心 (center) 为 (150, 100) ,半…

STM32F1笔记(一)GPIO输出

GPIO:General Purpose Input Output (通用输入/输出)。 GPIO最经典应用:LED灯。 先看电路。声明:参考正点原子战舰开发板。 与LED串联的电阻称为限流电阻。 限流电阻计算公式:R(U-LED压降)/20ma。 U为LE…

dataframe转化为array_【Python专栏】12 种高效 Numpy 和 Pandas 函数为你加速分析

来源:机器之心编译:Jamin、杜伟、张倩我们都知道,Numpy 是 Python 环境下的扩展程序库,支持大量的维度数组和矩阵运算;Pandas 也是 Python 环境下的数据操作和分析软件包,以及强大的数据分析库。二者在日常…

具有GlassFish和一致性的高性能JPA –第1部分

您以前听说过连贯性吗? 大概是。 它是那些著名的内存网格解决方案之一,该解决方案承诺了超快的数据访问速度和对经常使用的数据的无限空间。 一些众所周知的竞争对手是Infinispan , Memcached和Terracotta Ehcache 。 它们都很棒,…

boost原理与sklearn源码_机器学习sklearn系列之决策树

一、 Sklearn库 Scikit learn 也简称 sklearn, 自2007年发布以来,scikit-learn已经成为Python重要的机器学习库了。支持包括分类、回归、降维和聚类四大机器学习算法。还包含了特征提取、数据处理和模型评估三大模块。sklearn是Scipy的扩展,建立在NumPy和…

STM32F1笔记(二)GPIO输入

STM32 GPIO输入的经典应用是按键。 先看电路。声明:参考正点原子战舰开发板。 在这里可以看到,KEY_UP按键是高电平有效的,即当按下该按键时,GPIO读到高电平。 KEY0/1/2是低电平有效的,即当按下该按键时,G…

STM32F1笔记(三)UART/USART

UART:Universal Asynchronous Receiver/Transmitter(通用异步收/发器) USART:Universal Synchronous/Asynchronous Receiver/Transmitter(通用同步/异步串行收/发器) 从命名即可看出USART就是UART的基础上…

python安装界面翻译_python环境搭建

如果想要运行python需要有解释器和编辑器。 什么是解释器 解释器我们可以把它理解成翻译官,它是将我们写的python代码翻译成计算机能够懂得机器语言。 然后计算机收到解释器的命令来干活,最终再将结果反馈在解释器中。 解释器推荐使用anaconda3 什么是an…

进阶篇-用户界面:4.Android中常用组件

1.下拉菜单 在Web开发中&#xff0c;HTML提供了下拉列表的实现&#xff0c;就是使用<select>元素实现一个下拉列表&#xff0c;在其中每个下拉列表项使用<option>表示即可。这是在Web开发中一个必不可少的交互性组件&#xff0c;而在Android中的对应实现就是Spinne…

http的“无连接”指的是_http协议无状态中的 quot;状态quot; 到底指的是什么?...

引子&#xff1a;最近在好好了解http&#xff0c;发现对介绍http的第一句话【http协议是无状态的&#xff0c;无连接的】就无法理解了&#xff1a;无状态的【状态】到底指的是什么&#xff1f;&#xff01;找了很多资料不仅没有发现有一针见血正面回答这个问题的&#xff0c;而…

个人日志-7.4

姓名 刘鑫 时间 2016.7.4 学习内容 完善需求分析报告。撰写数据库设计说明书。初步安排计划概要设计说明书。调整项目开发计划说明书。 所遇问题 无 解决方案 无 转载于:https://www.cnblogs.com/liuxin13070013/p/5641967.html

STM32F1笔记(五)外部中断EXTI

STM32的每个IO都可以作为外部中断的中断输入口。 STM32F103的中断控制器支持19个外部中断/事件请求。每个中断设有状态为&#xff0c;每个中断/事件都有独立的触发和屏蔽设置。 STM32F103的19个外部中断为&#xff1a; EXTI线0~15&#xff1a;对应外部IO口的输入中断。 EXT…

STM32F1笔记(六)独立看门狗IWDG

STM32F1内置了两个看门狗&#xff0c;独立看门狗IWDG和窗口看门狗WWDG&#xff0c;可以用来检测和解决由软件错误引起的故障。 IWDG最适合应用于那些需要看门狗作为一个在主程序之外&#xff0c;能够完全独立工作&#xff0c;并且对时间精度要求较低的场合。WWDG最适合那些要求…

在JSF 2中对定制验证器进行参数化

在JSF 2中编写自定义验证器并不复杂。 您实现Validator接口&#xff0c;添加FacesValidator批注&#xff0c;并在faces-config.xml中插入Validator声明&#xff0c; 仅此而已 。 一块蛋糕。 但是&#xff0c;让我们考虑以下情形&#xff1a; 您需要自定义日期验证器&#xff0c…

STM32F1笔记(七)WWDG窗口看门狗

窗口看门狗与独立看门狗最大的不同是中断&#xff0c;窗口看门狗拥有一个提前唤醒中断。也就是在快要产生复位的前一段时间&#xff08;T[6:0]0x40&#xff09;来提醒需要进行喂狗&#xff0c;否则将复位。因此当窗口看门狗的计数器值减到0x40的时候&#xff0c;产生中断&#…

groovy怎样从sql语句中截取表名_Mysql和SQL

基本概念1.数据库DataBase简称&#xff1a;DB2.什么数据库&#xff1f;——用于存储和管理数据的仓库。存储过程是一个预编译的SQL语句&#xff0c;优点是允许模块化的设计&#xff0c;就是说只需创建一次&#xff0c;以后在该程序中就可以调用多次。3.数据库的特点&#xff1a…

STM32F1笔记(八)时钟

STM32有五种时钟源 1、HSI。高速内部时钟&#xff0c;RC振荡器&#xff0c;频率为8MHz。 2、HSE。高速外部时钟&#xff0c;可接石英/陶瓷谐振器&#xff0c;或者借外部时钟源&#xff0c;频率范围为4MHz~16MHz。 3、PLL。锁相环倍频输出&#xff0c;其时钟输入源可选择为HS…

java常用工具类(一)

一、String工具类 package com.mkyong.common; import java.util.ArrayList; import java.util.List; /** * * String工具类. <br> * * author 宋立君 * date 2014年06月24日 */ public class StringUtil { private static final int INDEX_NO…

python可以下载百度文库_不用下载券也能下载百度文库资料,Python帮你轻松搞定...

大家可能平时都有在百度文库下载文档的经历&#xff0c;费尽心思好不容易在文库找了一份可以用的资料&#xff0c;一看需要用下载券下载&#xff0c;搞的人很烦。 有的人为了节省时间&#xff0c;就任性办理了个文库VIP&#xff0c;再也不用纠结怎么下文档了。如果你是一个百度…

STM32F1笔记(九)通用定时器

STM32F1的定时器分为TIME1/8等高级定时器&#xff0c;TIME2~5等通用定时器&#xff0c;TIME6/7基本定时器。三者的区别主要参考《STM32中文参考手册》里特性的每种定时器主要特性的异同。 先看中文手册中通用定时器的简介&#xff1a; 通用定时器的寄存器有一个比较有趣的地方…