[导读]:超平老师的Scratch蓝桥杯真题解读系列在推出之后,受到了广大老师和家长的好评,非常感谢各位的认可和厚爱。作为回馈,超平老师计划推出《Python蓝桥杯真题解析100讲》,这是解读系列的第31讲。
最小剩余空间,本题是2020年6月20日举办的第11届蓝桥杯青少组Python编程省赛真题,题目要求编程计算空间问题,在n个物品中,任取若干个装入容器内,如何使容器的剩余空间为最小。
先来看看题目的要求吧。
时间限制:4000Ms
内存限制:589824K3
编程实现:
现有一个容器,其容量为V(0 < V < 1001,正整数),同时有n个物品(0< n ≤ 30),每个物品体积大小不同(正整数)。在n个物品中,任取若干个装入容器内,使容器的剩余空间为最小。
输入描述:
输入容器大小V(0 < V < 1001,正整数)
输入物品数量n(0 < n ≤ 30)
输入n个物品的不同大小(正整数)
输出描述:
剩余最小空间值
样例输入:
100
4
50
20
45
19
样例输出:
5
样例输入说明:
"100"输入的是容器大小V,"4"输入的是物品数量n,"50", "20","45","19"输入的是4个物品体积。
样例输出说明:
“5”是容器大小100减掉4个物体不同组合最为接近容器大小的一组值。(物品组合个数不限制,只找组合后最接近容器大小的值)
评分标准:
20分:能正确输出一组数据;
20分:能正确输出两组数据;
20分:能正确输出三组数据;
20分:能正确输出四组数据;
20分:能正确输出五组数据。
这是一道算法题,考查的知识点涉及枚举、组合算法和动态规划算法。
经典的装箱问题,这是信奥一本通书中的一道原题,题号是1295。对于C++而言,解决的方法是动态规划,对于Python而言,除了动态规划,还可以使用组合算法来实现。
首先,我们要彻底理解题目的意思,在n个物品中,任取若干个装入容器内,使容器的剩余空间为最小。容器的容量是固定的,剩余空间最小,就意味着物品的组合要占用最大的空间。
需要注意,每个物品最多只能取1次,要么不取,要么就取,就只有两种不同的状态。
如果将物品的体积当作价值,问题就变成了在固定大小的容器中装入价值最大的物品,这不就是鼎鼎大名的01背包吗?
接下来,我们分别使用组合和动态规划两种算法来分析其实现思路。
1.组合算法
组合算法相对比较容易理解,就是找出所有的组合,看看在这些组合中,满足总体积小于V的组合中,哪个的总体积最大。
以题目给出的样例数据进行分析,容器的容量为100,4个物品的体积大小分别为50、20、45和19。
由于物品可以是任意组合的,我们分4种情况来讨论。
1). 只选取一个物品时,有4种组合,如下:
(50)、(20)、(45)、(19);
其中,总体积最大的是50,即只选第一个物品。
2). 选取两个物品时,有6种组合,如下:
(50,20)、(50,45)、(50,19)、(20,45)、(20,19)、(45,19)
其中,总体积最大的是95,即选择1和3两个物品。
3). 选取3个物品时,有4种组合,如下:
(50,20,45)、(50,20,19)、(50,45,19)、(20,45,19)
其中,组合(50, 20, 45)和(50, 45, 19)的总体积超过100,直接忽略,在剩下的两组中,总体积最大的是89,即选择1、2、4这3个物品。
4). 选取4个商品,只有一种组合,如下:
(50,20,45,19);
组合的总体积超过100,可以忽略。
由此,我们可以得出,在不超过100的组合中,选择50和45这两个物品的总容量是最大的,总体积是95,因此剩余空间是100 - 95 = 5。
对于组合的实现,我们可以自己编写代码来实现,但是Python已经提供了内置模块itertools库,可以高效地实现数据的排列组合。
关于itertools库的使用,在《排列组合-第11届蓝桥杯选拔赛Python真题精选》做过详细介绍,这里就不再赘述了。
2. 动态规划算法
动态规划的核心思想是将原问题拆解成若干个重叠的子问题,通过将子问题的解存储起来,避免重复计算,从而提高了算法的效率,其中的核心找到递推公式(状态转移方程)。
对于动态规划,通常可以分成如下4个典型步骤:
确定dp数组以及下标的含义
确定递推公式
初始化dp数组
确定遍历顺序
对dp数组的确定最为关键,装箱问题需要考虑两个维度,分别是物品和体积,因此需要使用二维数组dp[i][j]来表示,其意思表示在大小为j的容器中,任取0~i中的物品组合时的最大体积。
这里的i表示在前i个物品中任意组合,j则表示容器的大小,其取值有讲究,通常是每次增加1。
为了方便说明,我们换一组数据,假定总容量为10,有3个物品,其体积分别是5、8、4,可以制作表格如下:
这是一张二维表格,行代表物品,列代表容器的容量,动态规划的过程其实就是如何填写这张表格的过程,填写过程中,始终牢记dp[i][j]的含义。
其中,物品0表示没有装入任何物品,体积0表示容器大小为0,在这两种情况下,结果都为0,所以都初始化为0。
最终,我们要计算出3种物品装入大小为10的容器中的最大体积,这就意味着i = 3,j = 10,对应于dp[3][10],也就是上面表格中最右下角的单元格。
这里的推导公式是什么呢?
此时,我们要使用拆解思维,将大问题拆分成小问题。再次牢记dp[i][j]的含义,它是指前i个物品装入大小为j的容器中的最大值。
不妨反问一下,对于第i个物品,我们到底要不要装入到大小为j的容器中呢,无非就是两种情况:
不装
装入
什么时候不装呢?当然是物品的大小超过当前容器的大小了。
比如将大小为5的物品装入容量为4的容器中,肯定装不下,此时dp[i][j] = dp[i -1][j],dp[i-1][j]的意思是指任意取前i-1个物品组合装入大小为j的容器中的最大体积。
dp[i][j] = dp[i-1][j]
从二维表格的角度来讲,dp[i-1][j]就是dp[i][j]正上方的单元格,这一下好理解了吧?
如果第i个物品的大小小于当前容器的大小,此时就要考虑一下,是装入划算呢,还是不装入划算呢?
如果要装入的话,那么首先就要将自己的体积累加起来,与此同时,它有可能会把其它物品挤出来,这就意味着前i-1个物品在容量只有(j - vi)的最大体积是dp[i-1][j-vi],其中vi是指第i个物品的体积。
是装入划算呢,还是不装入划算呢?
这是个问题,将两种情况拿出来较量一下,取较大的那一个就行,所以,其递推公式如下:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-vi] + vi)
注意,上面的vi应该是vi。
有了递推公式,就可以按照物品从上到下依次遍历,对于每个物品从左到右依次遍历,这是一个嵌套循环。
按照这个思路,可以将上面的表格填充完整,如下:
强烈建议你按照上面的分析过程,一步一步地来填写这个表格,表格最右下角的单元格就是最大体积值。
思路有了,接下来,我们就进入具体的编程实现环节。
根据上面的思路分析,我们使用两种方法来编写程序:
组合算法;
动态规划算法;
1. 组合算法
根据前面的思路分析,编写代码如下:
代码不难,简单说明5点:
1). 在输入物品体积的时候,一行一个数据,因此需要使用循环逐个获取并存入列表;
2). combinations()函数有两个参数,第一个参数是可迭代对象,第二个参数是组合的个数,对于n个物品,可以选1个、2个、3个....n个,所以需要使用循环来处理;
3). combinations()函数返回的结果是可迭代对象,为了方便处理,这里使用了list()函数将其转成列表,并追加到res列表中;
4). 在组合过程中,并没有考虑超过v的组合,因此需要单独处理,这里使用了列表推导式,将过滤后的组合进行累加,并存入到列表res1中;
5). 使用max()函数获取最大体积,然后使用v减去最大体积,就是剩余的最小空间了。
2. 动态规划算法
根据思路分析,我们编写代码如下:
代码还是挺有难度的,简单说明两点:
1). 根据前面的思路分析,为方便计算,增加了物品0和体积0,因此二维数组的长度需要在给定值的基础上加1;
2). 构造dp数组的时候,使用了列表推导式;
实际上,对于二维数轴的实现,我们可以使用滚动数组的思想进行压缩和简化,然后通过一维数组来实现,其代码如下:
关于滚动数组的原理和实现过程,这里就不再展开了,后续超平老师会开辟专题来讲述,代码理解起来有些难度,先参考一下吧。
接下来是测试环节了。
使用题目的样例数据,总容量为100,有4个物品,其体积分别是50、20、45、19,结果如下:
假定总容量为10,有3个物品,其体积分别是5、8、4,结果如下:
假定总容量为24,有6个物品,其体积分别是8、3、12、7、9、7,结果如下:
至此,整个程序就全部完成了,你也可以输入不同的数字来测试效果。
本题的分数为100分,代码在15行左右,涉及到的知识点包括:
循环语句,主要for...in循环;
输入输出处理;
系统函数的使用;
列表运算;
组合函数;
动态规划算法;
作为高级组最后一题,难度较大,虽然代码不多,这里的难点是动态规划算法的理解和实现。
从题目本身的角度来讲,这是一个组合问题,通常可以使用枚举来实现,但是题目给出的v和n都是变化,使用枚举比较麻烦,而且枚举的效率不高,尤其是对于数据量较大的情况,考试时存在超时情况。
针对排列组合,在Python编程中,最简单的实现方式就是使用itertools库,重点是combinations()和permutations()两个函数。
在一般情况下,combinations函数的效率是很高的,因为它使用了高效的算法来生成组合,并且在实现上进行了优化。
然而,当元素数量较大或者要生成的组合数量非常庞大时,其效率会受到影响。这是因为生成所有可能的组合需要消耗大量的计算资源和内存空间。
在实际使用中,如果需要生成大量组合,可以考虑对算法进行优化,或者使用其他更高效的方法来处理。
作为经典算法,动态规划还是有些难度的,在学习的时候,一定要彻底理解dp数组的含义,结合二维表格来理解,最好的方法就是自己绘制二维表格,一步一步地填写好表格,关于动态规划,超平老师后面会有专题介绍,请持续关注。
超平老师给你留一道思考题,将二维数组简化为一维数组,其原理是什么,需要注意什么问题?
你还有什么好的想法和创意吗,也非常欢迎和超平老师分享探讨。
如果你觉得文章对你有帮助,别忘了点赞和转发,予人玫瑰,手有余香。
有需要源代码的,可以移步至“超平的编程课”gzh。