动态规划——耐摔指数
代码实现:python
今天来和大家看一下耐摔指数这道题目。
这道题目看起来蛮简单的,它算是我学习动态规划时看的第一道例题。
但是时隔好几个月,当我第一次参加了蓝桥杯python组,用动态规划解出几道题目,再回过头来看这道题目,发现之前自己只是从一个十分模糊的角度来思考这道题目,所以对题目的认知也是感性的,不明确的。
最近,机缘巧合,在我在准备一次答辩,试图从原点出发,一步步理清解题思路时,才真正认识到这道题目的巧妙之处。后来看了很多别人的题解,其中有正确的,也有错误的,但多多少少给了我一些启发。
所以现在,我想尝试一下,用一种所谓“合情”“合理”的方式,来解读这道题目。
题目描述(简介)
x星球有很多高耸入云的高塔,刚好可以用来做耐摔测试。
塔的各层高度都是一样的。他们的第 1 层不是地面,而是相当于我们的 2 楼。
他们的地面层为 0 层。如果手机从第7层扔下去没摔坏,但第8层摔坏了,则手机耐摔指数 = 7。
特别地,如果手机从第 1 层扔下去就坏了,则耐摔指数 = 0。
如果到了塔的最高层第 n 层扔没摔坏,则耐摔指数 = n。
为了加快测试进度,从每个型号手机中,抽样 3 部参加测试。
问题来了:如果已知了测试塔的高度 n,并且采用最佳的策略,在最坏的运气下需要测试多少次才能确定手机的耐摔指数呢?
题目分析——简析
1.这道题目到底说了一件什么事呢?
首先,它定义了一个指标,叫做难摔指数。
就是:对与一种手机,从第x层摔下,没摔碎;从第x-1层摔下,摔碎了,则其耐摔指数为x。也就是说,把手机从楼上摔下,它能保证 不被摔碎 的最高的楼层就是耐摔指数x。
2.我们要做什么呢?
题目给出了参与测试的手机数(一共三部手机),也给出了参与测试的楼层数(楼高)(楼高将作为输入给出)。
而我们要使用最少的测试次数n,来测试出x。
3.限制条件(最坏运气、最优决策)
4.所求:最少测试次数n
题目分析——变量分析
变量分析是这道题目最基础的一步。
其实,在日常做题时,分析变量好像是一件理所当然、简单且没有亮点的事(绝大多数时候,它确实没有什么用),好像我们总是在不知不觉间就把它做了。
但是,当我们有一天真的认认真真去分析它的时候,也许会给我们一个不一样的视角。
1.设置变量
(1)参与测试的手机数k(手机总数是常量,但我们分析问题时,还是要把手机数作为一个变量来看。因为参与测试的手机数和手机总数并不是一个概念)
(2)参与测试的楼层数h(请注意,这里的楼层数并不是楼高)
(3)当前测试次数n
变量变量,它可以是静的,但是它一定要能动起来。
我们也可以设置一下常量。
比如:总手机数phone = 3;楼高height;最少测试次数(所求结果)res。
一道题目中,phone, height,res都是有固定值的。
2.因果关系
在题目中,量与量之间是有明确的因果关系的。
根据题目描述,对于一个给定的楼高height,一定有唯一的最少测试次数res与之对应。
很显然,在这里,height是自变量,res是应变量。(这里考虑的是不同题目间的普遍性的关系,而不是一道具体的题目,所以它们可以作为变量看待)
但实际上,我们发现:
对于一个测试次数res,我们也一定有唯一的最高层楼height与之对应。
也就是说,res与height间是一一对应的关系。
这反映了一个什么问题,就是说测试次数是可以作为自变量的。
3.变化率(手机数一定)
我们得知res和height之间具有一一对应的关系。有什么用?我们分别从两个不同的角度考虑一下。
(1)当height+1时,res一定会改变吗?不一定。
当只有两部手机时,无论要测试2层,还是3层,都只需要测试两次就能达到目的。
(2)当res+1时,height一定会改变吗?很显然,一定。
哪怕我只是随便扔一下,我也能多测出一层来。
所以,我们得知楼层的变化率是更大的,所以把n作为自变量分析问题更直观、更容易理解。
小结:
通过变量分析(就是前面的辣么多的废话),我们知道:
(1)n与h(res与height)是可以对调的。
(2)我们要把n作为自变量来分析问题。
题目分析——结构与性质
题目分析是在一道具体的题目中考虑的
前提条件:
对于给定的楼高h,有最小的测试次数n
对于给定的测试次数n,有最大的楼高h
做假设:假设要测试s次(n=s)
【目的】要使测试s次时,测出的楼层数最大。
【条件】很显然,前面s-1次测试一定要满足一个有关最大测试楼层的条件(不管用什么方法)
听起来很难理解对吧,那我们拿二分来举一个例子:假设用二分法测试楼层,h(s)=2*h(s-1),所以前面的s-1次测试,测出的楼层要最大,同时,前面的s-1次测试中,每一次测试都要最大。
【结论1】题目满足——最优子结构
【现象】(不论用什么方法测试)随着我测试次数的增加,当前测试的结果将作为大问题的子问题重复出现
这个怎么理解呢,我们依然那二分法举例子,h(s)=4*h(s-2),h(s-1)=2*h(s-2),所以h(s-2)將作为h(s)和h(s-1)的子问题出现
【结论2】题目满足——重叠子问题
最优子结构和重叠子问题共同决定了这道题目是一个多阶段决策问题。
方法选择:
首先,可以解决多阶段决策问题的方法有:分治;贪心;动态规划。
其中适用于最优子结构的有贪心和动态规划。
我们根据限定条件中的最用决策来否定贪心,因为贪心绝大多数情况下只能满足局部最优而不能满足整体最优
最后,我们根据重叠子问题来否定单纯的分治算法(没有否定分治思想!!!),因为分治会浪费大量的时间在重叠子问题上。
方法验证:
1.无后效性:前一步决策是否会对下一步决策产生影响。
我们现在还没有确定该如何划分子问题,如何决策,所以这一点暂时无法判断。
随着解题步骤的推进,我们发现题目是满足这样条件的,这一点,不会再赘述。
2.数据规模:判断一下数据规模是否满足。
很多情况下,动态规划是一种以空间换时间的算法,所以我们要尤其注意来自额外空间的限制。
小结:
我们判断出题目具有多阶段决策、最优子结构和重叠子问题等性质,并依此判断出应当采用动态规划作为方法。
解题思路——动态规划
1.特殊情况
这没什么好说的
(1)当楼层只有一层时(h=1),一次就能测出来(n=1);
(2)当手机只有一个时,为了保证手机在测试完前不摔碎,我们只能从一楼开始,一层一层往上测,根据最坏运气的原则,此时测试次数就等于测试楼层h(n=h)。
2.思考:如何缩小问题规模?答案——测试。
无论是基于分治思想还是动态规划算法,思考如何缩小问题规模、划分子问题和最终每一步的决策都是不可避免的两个步骤。
在这里,很显然,无论是减少手机数(摔手机,摔碎了,手机数k-1)还是减少测试楼层(目标楼层一定位于测试楼层的上或下)都可以满足要求。
这就解释了为什么测试这么sb且无用的答案可以作为问题的解。
3.思考:如何决策?动态规划——状态转移方程。
到目前为止,我们对如何划分子问题还没有任何的思路。(动态规划就是填表格,表中每一个数据都可以视作一个子问题。但我们不知道,对于一个具体的问题,当前所求的数据是根据前面哪个格子中的数据得来的)
那么不妨顺着测试的思路想下去。那么对于当前h个测试楼层,我们该如何测试呢?
不知道,就假设。给要测试的h层编号1~h,设手机从第t层摔下。一共可以得到两种可能。
(1)摔碎了,手机数k-1,需要测试剩下的1~t-1层,即要测试t-1层
(2)没摔碎,手机数不变,要测试剩下的t+1~h层,即要测试h-t层
那么这两个状态该怎取呢?根据最坏运气原则,在状态转移这一步,只能取最大值。
用函数来表示就是n(k, h ,t) = 1 + max(n(k-1 ,t-1), n(k, h-t))
。
这就是我们说的状态转移方程。
4.思考:如何划分子问题
到了这里,我们有特殊情况下的数据,有了状态转移方程,还差怎么处理t这个参数,本质上就是要考虑该如何划分子问题。
如果这是贪心,我们会如何划分子问题呢?很显然,二分就是一种很直观的方式。为什么,因为当我们从上而下做决策的时候,二分是一种主观上看起来,是子问题重叠最小的方式。
但是,作为动态规划,我们必须要从下而上做决策。所以我们不得不选用一种看起来很笨的方法来处理它,那就是让t取遍1~h间的每一个值,然后再取答案的最小值。
用函数来表示就是n(k ,h) = min(n(k, h, t))(1<=t<=h)
小结:
这部分的处理时是解这道题最最核心的部分,但其实也是最好学的部分。它比前面那些飘渺的、不着调的变量分析、性质分析要更加具体明了,对解题也有最直接的帮助。所以我们再重复一遍:
(1)处理特殊情况
(2)缩小问题规模
(3)状态转移方程
(4)划分子问题
它们之间不一定有固定的顺序,但通常情况下,缺一不可,而且也足以处理绝大多数问题了。
完整代码:
import sys
if __name__ == '__main__':
# 手机数k,楼层高h, 次数n
phone = 3
height = int(input())
# 1.创建dp数组(表格)[横轴h][纵轴k]
dp = [[0 for h in range(height)] for k in range(phone)]
# 2.填充第一行[k=1, h变, n=h]
for h in range(height):
dp[0][h] = h+1
# 3.填充第一列[k变, h=1, n=1]
for k in range(phone):
dp[k][0] = 1
# 4.根据公式求取其它值
for h in range(1, height):
for k in range(1, phone):
minNum = sys.maxsize
for t in range(1, h+1):
minNum = min(minNum, 1 + max(dp[k-1][t-1], dp[k][h-t]))
dp[k][h] = minNum
print(dp[phone-1][height-1])
总结:通过这道题,我们究竟学会了什么?
1.要把变量分析作为一种习惯!!!
不要看我们前面做变量分析的时候BB了一大堆,其实在真的做题时,由于时间的限制,我们确实只能十分模糊的感知一下。但是我们可以养成一种习惯。
(1)在题目没有思路的时候,不妨认真的分析一下变量之间的关系。
(2)习惯性的考虑调换变量是否更加有助于解题(变化率有时可以给你一个理由)
2.分治yyds
很多时候,面对一个复杂的实际问题,我们都可以用分治的思想去思考解决方案。在面向过程的编程中,分治是你永远都绕不过去的坎。
根据分治思想,我们又发展出贪心思想、动态规划的思想,最后才是各自的算法。形式上,它可以是递归,也可以满足递推。
无论遇到什么问题,我们都可以从如何缩小问题规模、如何划分子问题来考虑。其中穿插着以贪心、动态规划为代表的自下而上或是自上而下做决策的各种思路。比如,在我们分析题目决策与性质的时候,我们总是在不经意间提到子问题,提到大问题与小问题间的关系;同时,在解释最优子结构和重叠子问题时,我们总是拿二分法举例,显然,二分是贪心在自上而下做决策时划分子问题的一种具体方法。事实上,我也很难解释,为何要从这个角度去考虑。只能再次提到那个词——习惯。
把分治,贪心,动态规划作为思考问题的一种习惯,无论是在做题中还是在生活中,你都会受益。
那么到这里其实也明确了一件事,就是在我们分析题目的性质与结构之前,我们就已经有了大体的方向,这个方向是经验给我们的。所以所谓分析题目的性质与结构这一步,只是做了一个简单的验证,就是题目满足多阶段决策。同时,排除了另外的两种算法。当然,这是很有必要的,只是,和前面的变量分析一样,它在解决实际问题的过程中,可以比想象的更加的精简。
3.划分子问题很难
对于初学动态规划的人而言(我现在就是),因为如何划分子问题可能直接决定了全局决策是否最优,所以可能一般要最后考虑(我现在见的题太少,也太菜,不知道以后是不是会有些不同的理解)。
4.学会假设。没有思路,不知道如何下手的时候,就假设,试试看。
这道题有两个假设,一个是在分析题目的性质与结构设了测试s次;一个是在求状态转移方程时设了测试楼层t。两个假设在解题过程中都起到了至关重要的作用。
5.处理问题
(1)处理特殊情况
(2)缩小问题规模
(3)状态转移方程
(4)划分子问题
6.学会利用感知
数学的感知其实对解题有很大的帮助(虽然很多情况下,它并不准)。
这道题目只是蓝桥杯中一道中等的题目(可能偏难一点,但事实上,它甚至没有出现在当年A组的试卷中)那么我大胆猜测一下真正的大佬是怎么解这道题的。
他们强大的实力可以为他们提供足够的容错率,所以他们不会去主动的思考变量关系和题目性质,只是凭借着直觉和经验就相对应的往分治上面靠,然后借贪心去思考问题(但不会用贪心),借动态规划(二维数组的额外空间)来优化分治,然后因为懒,直接遍历t划分子问题(意识会告诉他们不能直接二分取t、这也是他们否定贪心的原因)。
我还远远不能达到这个境界,只是对于一些简单的题,我开始有一些简单的近乎直觉的反应。这种反应令我着迷。
提升自己的感知、直觉是一件漫长的过程。对于一些基础不太扎实的同学,他们可能就会下意识的忽略一些因素,直接二分取t。所以,我们不能拘泥于提升感觉。在漫长的有逻辑有收获的练习中,感觉会慢慢提升的。