0-1背包问题是动态规划中入门的经典题型,掌握0-1背包问题背后的本质有助于更好地理解动态规划问题,话不多说,首先来看看0-1背包问题究竟是什么吧~
问题描述:设有n件物品x1, x2, …, xn,每件物品有一个价值和一个重量,分别记为v1, v2, …, vn和w1, w2, …, wn,其中所有的wi均为整数。现有一个背包,其最大载重量为m,要求从这n件物品中任取若干件(这些物品要么被装入要么被留下)。问背包中装入哪些物品可是得所装物品的价值和最大?
样例1输入:m=11, n=6
样例1输出:12
在不考虑时间的情况下,暴力枚举是一个很直观的方法,也就是枚举所有可能装入背包的物品组合,找出价值和最大的组合,只不过这里我们是用递归的方式来实现这个枚举。
我们首先定义递归函数,它表示在0至i号物品中,载重量为w时的所有可行方案的最大价值和,那么很容易得到递归关系式:,也就是在0至i号物品中,载重量为w的最大价值和等于0至i-1号物品中,载重量为w-wi的最大价值和加上vi的值和0至i-1号物品中,载重量为w的最大价值和中的较大值,python代码如下。
很显然,暴力枚举的复杂度是十分高的,那么我们可以如何去优化这个暴力枚举的复杂度呢?我们从一种简单的特殊情况来分析,假设0至n号物品的重量均为c,那么我们可以得到以下的递归树。
很显然,在经过2层递归后,就出现了重复的子问题,并且继续递归下去,重复的子问题会越来越多。因此,我们可以构造一个备忘录,对于重复求解的问题,计算一次后记入备忘录,下次遇到相同的问题,只需要从备忘录中查询就行。python代码也只需要在原先的基础上做简单的变动即可。
因为递归毕竟是递归,它的计算过程肯定首先是自顶向下递归到最原始的单一问题,之后再通过自底向上的递推过程,计算每一步的解,最终回归到了最初的问题。那么我是否们能够通过这个备忘录的数组直接自底向上计算呢?
答案当然是可以的,那就是动态规划。我们可以通过递推关系公式来逐行逐行的填充这个数组,最终数组最右下角的值就是最大价值和。
现在我们已经得到了最大价值,还有一个问题,那就是到底选择了哪些物品能够获得这个最大价值?我们可以用rec数组记录下决策过程,在填充p[i, j]时,如果选择了i商品,则rec[i, j]=1;反之,rec[i, j]=0。然后我们可以用rec数组来追踪最优解,一行一行往回倒推,当rec[i, j]=1时,选择商品i,考察子问题p[i-1, w-weight[i]];当rec[i, j]=0时,不选商品i,考察子问题p[i-1, c]。
以样例1为例,rec[5, 11]=0,说明没有选择5号商品,则rec[5, 11]是由rec[4, 11]推导而来的;再看rec[4, 11]=0,说明也没有选择4号商品,则rec[4, 11]是由rec[3, 11]推导而来的;再看rec[3, 11]=0,说明也没有选择3号商品,则rec[3, 11]是由rec[2, 11]推导而来的;再看rec[2, 11]=1,说明选择了2号商品,则rec[2, 11]是由rec[1, 11-weight[2]]即rec[1, 6]推导而来的;再看rec[1, 6]=1,说明也选择了1号商品,则rec[1, 6]是由rec[0, 6-weight[1]]即rec[0, 2]推导而来的;再看rec[0, 2]=1,说明也选择了0号商品。
于是,python的代码也就比较容易得到了。
动态规划算法的本质就是把原始问题分解成子问题,并寻找小问题和大问题之间的关系,从小问题向大问题的规模去扩展,最后达到最优解。总的来说,动态规划算法可以分解为4个步骤:
- 问题结构分析:描述一个最优解的结构;
- 递推关系建立:递推地定义最优解的值;
- 自底向上计算:以“自底向上”的方式计算最优解的值;
- 最优方案追踪:从已计算的信息中构建出最优解的路径。
在本题中,问题结构分析就是给出0-1背包问题的表现形式,也就是表示在0至n-1号物品中,载重量为w时的所有可行方案的最大价值和;递推关系建立需要去分析最优子结构(最优子结构性质就是指问题的最优解由相关子问题最优解组合而成,子问题可以独立求解),在本题中,是否取第i件商品就构成了原问题的两个相关子问题,然后两个相关子问题的最优解的较大值也就是原问题的最优解,于是递推关系也就得到了,;再得到递推关系后,自底向上计算就很容易了,只要在初始化后,根据递推关系公式就可以依次求解问题了;如果需要输出最优方案,那么就需要使用rec数组来追踪最优方案,rec数组记录了决策的过程。
版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.mushiming.com/mjsbk/3734.html