实验内容:
动态规划将问题划分为更小的子问题,通过子问题的最优解来重构原问题的最优解。动态规划中的子问题的最优解存储在一些数据结构中,这样我们就不必在再次需要时重新处理它们。任何重复调用相同输入的递归解决方案,我们都可以使用动态规划对其进行优化。鸡蛋掉落问题是理解动态规划如何实现最佳解决方案的一个很好的例子。问题描述如下:
我们需要用鸡蛋确认在多高的楼层鸡蛋落下来会破碎,这个刚刚使鸡蛋破碎的楼层叫门槛层,门槛楼层是鸡蛋开始破碎的楼层,上面所有楼层的鸡蛋也都破了。另外,如果鸡蛋从门槛楼层以下的任何楼层掉落,它都不会破碎。如上图所示,如果有 5 层,我们只有1个鸡蛋,要找到门槛层,则必须尝试从每一层一层一层地放下鸡蛋,从第一层到最后一层,如果门槛层是第 k 层,那么鸡蛋就会在第 k 层抛下时破裂,应该做了k次试验。
注意:我们不能随机选择任何楼层,例如,如果我们选择 4 楼并放下鸡蛋并且它打破了,那么它不确定它是否也从 3 楼打破。 因此,我们无法找到门槛层,因为鸡蛋一旦破碎,就无法再次使用。
给定建筑物的一定数量的楼层(比如 f 层)和一定数量的鸡蛋(比如 e 鸡蛋),找出阈值地板必须执行的最少的鸡蛋掉落试验的次数,注意,这里需要求的是试验的测试,不是鸡蛋的个数。还要记住的一件事是,我们寻找的是找到门槛层所需的最少鸡蛋掉落试验次数,而不是门槛层下限本身。
问题约束条件:
- 从跌落中幸存下来的鸡蛋可以再次使用。
- 破蛋必须丢弃。
- 摔碎对所有鸡蛋的影响都是一样的。
- 如果一个鸡蛋掉在地上摔碎了,那么它从高处掉下来也会摔碎。
- 如果一个鸡蛋在跌落中幸存下来,那么它在较短的跌落中也能完整保留下来。
暴力枚举:
- 思路:从最高层开始尝试,一层一层地扔鸡蛋,直到找到鸡蛋摔碎的楼层为止。这种方法的最坏情况下需要尝试所有可能的楼层,例如当f=7,试到第一层最低层鸡蛋才碎(k=1),此时res=f即楼层数量。
- 伪代码:
暴力枚举(线性扫描) |
Int linearSearch(int f) { num=0 // 尝试次数 For i from f to 1 dec : num++ If i==k: Return num // 试到鸡蛋碎的楼层了,直接可以返回 Else : Continue // 鸡蛋没碎,继续尝试 Return num // 如果到达最后,直接返回尝试次数 } |
- 思考分析:由于从跌落中幸存下来的鸡蛋可以再次使用,考虑从最高层往下扔鸡蛋,这样可以不受鸡蛋个数的限制。但这种线性扫描暴力枚举的方法当楼层数很大时,效率会非常低下。
暴力二分:
- 思路:将搜索范围二分,每次选择中间楼层扔鸡蛋,如果鸡蛋碎了则往高楼层找鸡蛋恰好没碎的楼层h;如果鸡蛋没碎则往低楼层找鸡蛋恰好碎了的楼层k,即根据鸡蛋是否摔碎来确实下一步搜索的范围。例如当f=7,首先去第(1+7)/2=4层扔一下,如果碎了说明k>=4,则去第(5+7)/2=6层尝试;如果没碎说明k<4,则去第(1+3)/2=2层尝试。这种方法的最坏情况下需要尝试次数为log7向上取整等于3次。
- 伪代码:
暴力二分 |
|
- 思考分析:如果不限制鸡蛋个数的话,二分显然可以得到最少尝试次数,但限制鸡蛋个数为e,直接使用暴力二分并不可行。例如,如果f=100,e=2,先在第50层扔鸡蛋,如果碎了最后一个鸡蛋只能线性扫描51-100层找到没碎的临界层,如果没碎最后一个鸡蛋也要线性扫描1-49层找到碎的临界层,无论如何最坏情况都要扔50次。在限制了鸡蛋数量的情况下,暴力二分搜索也不是一个高效的解决方法。
动态规划
- 设F(e,f)表示目前有e个鸡蛋,需要测试的楼层数量为f的最坏情况下最少操作次数。目前在x层,可以分成两个子问题F(e-1,x-1)和F(e,f-x),转移到当前F(e,f)状态。
- 由于鸡蛋在x层,如果碎了则往上面高楼层也是碎了的,而下面低楼层中存在没碎的临界层;如果没碎则往下面低楼层也是没碎,而上面高楼层中存在碎的临界层。
- 因此,子问题F(e-1,x-1)表示目前在x层碎了,还剩e-1个鸡蛋,还有x-1层楼需要测试的最坏情况下最少操作次数(即要去下面x-1层楼测试找到临界没碎的那一层);子问题F(e,f-x)表示目前在x层没碎,还剩e个鸡蛋,还有f-x层楼需要测试最坏情况下最少操作次数(即要去上面f-x楼测试找到临界碎的那一层)。
- 由于F(e,f)求的是鸡蛋个数为e,楼层数量为f的最坏情况下最少操作次数。最坏情况下,即当从两个子问题中选一个子问题转移到主问题F(e,f)中时,我们要选择需要操作次数多的那个子问题,确保是最糟糕情况(要操作多次)下的转移。
- 在第x层操作后,我们考虑是去x的上面楼层继续试还是下面楼层继续试,就去所需操作次数多的那一方。如果在上面楼层试的操作次数多,就让鸡蛋不碎;如果下面楼层试的操作次数多,就让鸡蛋碎。
思路:动态规划问题主要分成三部分:状态表示,转移方程,初始和边界条件。
状态表示:根据题目,鸡蛋个数为e,楼层数量为f,转换为一个函数F(e,f),F(e,f)表示目前有e个鸡蛋,需要测试的楼层数量为f的最坏情况下最少操作次数。
转移方程:假设我们第一个鸡蛋扔出的位置在第x层(1<=x<=f),有两种情况:
- 第一个鸡蛋没碎,剩下f-x层楼,剩下e个鸡蛋,函数转变为:F(e,f-x)+1。
- 第一个鸡蛋碎了,剩下x-1层楼,剩下e-1个鸡蛋,函数转标为:F(e-1,x-1)+1。
- “至少”的理解:满足要求的最大尝试次数的最小解。F(e-1,x-1)和F(e,f-x)这个两个函数,固定e和f,当x从1到f单调递增,前者随着x的增加也单调递增,后者随着x的增加而单调递减。
- 整体而言,要求出的是在e个鸡蛋,f层楼的条件下的最大尝试次数的最小解,转移方程为:F(e,f) = min( F(e,f) , max( F(e,f-x) , F(e-1,x-1) )+1 )
初始和边界条件
- 当e=0且f=0时,无需操作。
- 当e=1即只有一个鸡蛋时,res=f,即线性扫描所有楼层。
- 当f=1即只有一层楼时,res=1,扔一次即可。
- 在每次选择楼层进行扔鸡蛋之前,操作次数初始为当前楼层的数量+1(最坏结果),之后再进行取min操作即可。
伪代码:
动态规划(递归处理) |
|