开篇

经过了各种惨痛的公司笔试失利之后,慢慢掌握了公司出题的一些套路,其中动态规划是避免不了的。最近研究了一些公司笔试的一些出题思路和套路,加上自己看的一些大神的面经,打算写一些关于公司常见笔试类型题的系列文章。这一部分本身也是自己的薄弱项,所以打算写的细致一些,希望对大家有所帮助,也希望自己早点摆脱被虐的困境。
声明:我列出的这些不是百分百会出的题,只是我个人遇到的比较多的问题,或者是我遇到的问题的基础问题,弄懂了基础问题,扩展的问题就不在话下了。还有,奉劝大家认真搞懂基础算法,无论是做算法还是开发(尤其算法),不要去那些刷题网站学习别人的奇淫技巧,从最根本最基础的解法出发才是真正学到东西了。虽然本人啥也不会,但是道理懂啊嘿嘿嘿
废话少说我们正式开始第一篇专题:动态规划的高楼扔鸡蛋问题。

问题描述

若干层楼,若干个鸡蛋,让你算出最少的尝试次数,找到鸡蛋恰好摔不碎的那层楼。国内大厂以及谷歌脸书面试都经常考察这道题,只不过他们觉得扔鸡蛋太浪费了,改成扔杯子什么的(一样浪费)。
这道题目的解题方法很多,各种奇淫技巧也是层出不穷,我们下面就用动态规划的思路来解决一下这道题。

题目分析

你面前有一栋从1到N共N层的楼,然后给你K个鸡蛋(K至少为1).现在缺定这栋楼存在楼层0<=F<=N,在这层楼将鸡蛋扔下去,鸡蛋恰好没碎(高于F的楼层都会碎,低于F的楼层都不会碎)。现在问你,最坏情况下,你至少要扔几次鸡蛋才能确定楼层F呢?
我们提炼一下这里面的关键字,什么叫做最坏情况下,至少…
究竟什么情况是最坏情况呢,假设我们面前有10层楼,我们不限制鸡蛋的数目,我们应该如何找到在第几层楼鸡蛋会碎呢。
我们会这样选择:在1楼扔,没碎。2楼扔,没碎.3楼扔,没碎…最后到了第十层鸡蛋也没碎,这样的话我就扔了10次鸡蛋,这就是我们所说的最差情况。
我们都知道这就是最差最差的情况了,但是傻子才会一层一层地尝试呢,这让我们想起了作为程序员刚入门的时候的例子:
在你经过图书馆出口的时候,警报器响了,图书管理员怀疑你偷了书,于是管理员大妈把100本书都拿了出来,依次让他们通过出口,看是否警报会响起。你在一旁偷笑,因为你知道,这会耗费它大量的时间。而过了一会,另一位管理员大妈来了,它把书分成50,50,依次通过出口,将警报响起的一摞再分成25,25依次通过…不一会就找出了被你"偷"走的书。
这个例子就是我们入门二分查找的一个启蒙,所以这道题目,如果我们逐楼去尝试,那就是第一个大妈,所以我们想到了一种优化方法,先在(1+10) / 2 = 5层扔一下,如果碎了说明F小于5,我们就去(1+4)/ 2=2层尝试扔一下…
如果没碎说明F大于5,我们就去(6 + 10) / 2 = 8层尝试扔一下…
这个方法就解决了至少扔几次的问题。
但是别忘了,我们的前题是不限制鸡蛋的个数,如果现在给出了鸡蛋个数的限制为K,直接使用二分思路就不行了。比如说现在10层楼,给你1个鸡蛋,我不相信你还敢这么二分着扔。扔一次直接回到解放前,你只能把自己想象成鸡蛋跳下去了。
开个玩笑,我想说的是,这一类问题不会这么轻易让你用这么简单的二分法解决的,因为我们的限制条件比较多,也就是说我们的"状态比较多",例如这道题里面状态是什么呢?——楼层数,鸡蛋数,尝试次数。这些都是我们的状态。既然想到了状态,那就一定要想到适用于状态转移的万能解题方法——动态规划法。

动态规划法基本框架

对于动态规划法来说,先给出一个万能的基本框架:这个问题有什么[状态],有什么[选择],然后穷举。
状态我们已经说过了,当前拥有的鸡蛋数K和需要测试的楼层数N。随着测试的进行,鸡蛋个数可能减少,楼层的搜索范围会减小,这就是状态的转移。
选择其实就是去选择哪层楼扔鸡蛋。
于是我们动态规划的基本思路就形成了,肯定是一个dp二维数组来表示状态转移;外加一个for循环来遍历所有选择,择最优的选择更新状态:

# 当前状态为K个鸡蛋,面对N层楼
# 返回这个状态下的最优结果
def dp(K,N):res = 10000for i in range(1,N+1):res = min(res,这次在第i层楼扔鸡蛋)return res

好吧我知道,这一段很难看的伪代码,因为我们没有看到递归和状态转移,但是这就是算法的框架,穷举+选择,我们只需要填上东西就好了。
我们选择在第i层楼扔了鸡蛋之后,可能出现两种情况:鸡蛋碎了,鸡蛋没碎。这时候状态转移就出现了:
如果鸡蛋碎了,那么鸡蛋的个数K就应该-1,搜索的楼层区间应该从[1,…,N]变为[1,…,i-1]共i-1层楼
如果鸡蛋没碎,那么鸡蛋的个数K不变,搜索的楼层区间应该从[1,…N]变为[i+1,…N]共N-i层楼

我自己在看这一部分的时候有一个问题,我觉得肯定有一部分朋友和为有相同的问题,在这里先说明一下:
在第i层楼扔鸡蛋如果没碎,楼层的搜索区间缩小至上面的楼层,是个不是应该包含第i层楼呢?不用,因为已经包含了,开头说了F是可以等于0的,向上递归后,第i层楼其实就相当于第0层,可以被取到,所以说并没有错误。
现在我们回到原问题,找出最坏情况下的至少扔鸡蛋次数。我们可以肯定这是一个最小最大问题。

def dp(K,N):for 1<=i<N:res = min(res,max(dp(K-1,i-1),#碎,鸡蛋数-1dp(K,N-i)# 没碎) + 1)return res

递归的base case很容易理解:当楼层数N等于0时,显然不需要扔鸡蛋;当鸡蛋数K=1时,只能线性搜索所有楼层。

def dp(K,N):if K == 1:return Nif N == 0:return 0

至此这个问题就顺利解决了

def superEggDrop(K,N):memo = {}# 用字典存储,方便查找def dp(K,N):if K == 1:return Nif N == 1:return 0# 避免重复计算if (K,N) in memo:return memo[(K,N)]res = float("inf")# 穷举所有可能for i in range(1,N+1):res = min(res,max(dp(K-1,i-1),dp(K,N-i)) + 1) # 加1表示第i层扔鸡蛋memo[(K,N)] = resreturn resreturn dp(K,N)

这个算法的时间复杂度是多少呢?
动态规划算法的时间复杂度就是子问题个数*函数本身的时间复杂度,因为我们是穷举每一个选项,也就是每一个子问题都需要去执行一次函数。dp函数中有一个for循环,所以函数本身的复杂度为O(N)。子问题个数也就是不同状态组合的总数,显然是两个状态乘积,也就是O(KN).所以算法的总时间复杂度是O(KN^2),空间复杂度为O(KN)

进阶篇

二分查找法
说完了上一种算法的时间复杂度,相信很多朋友都不会满意的,因为这个时间复杂度还是蛮大的。现在我们应该相一个方法尽可能地缩短这个时间。
我们注意到,我们在dp函数中有一个for循环,这个for循环是一个线性地查找,依次楼层扔鸡蛋然后找到刚好破碎的位置,这个步骤确实有些浪费时间,所以我们的想法是找到一个可以缩短这个查找时间的方法。
我们可以快速想到上文提到的二分查找法。

由图可知,随着dp(K-1,N-1)和dp(K,N-i)的变化,画出max(dp(K,N-i),dp(K-1,N-1))的曲线可以快速找出最小值,这个最小就是一个Valley值,我们也称为山谷值。我们可以使用二分法快速找到这个点。

def superEggDrop(K,N):memo = dict()def dp(K,N):if K ==1:return Nif N == 0:return 0if (K,N) in memo:return memo[(K,N)]lo,hi = 1,Nres = float("inf")while lo <= hi:mid = (lo + hi) // 2;broken = dp(K-1,mid-1) # 碎not_broken = dp(K,N-mid) # 没碎if broken > not_broken:hi = mid - 1res = min(res,broken + 1)else:lo = mid + 1res = min(res,not_broken + 1)memo[(K,N)] = resreturn resreturn dp(K,N)

解法框架很清晰明了了:找状态,做选择
这个方法由于查找的时间复杂度为O(logN),所以总的时间复杂度为O(KNlogN)
其实还有一种数学方法,可以将时间复杂度降低至O(K*logN),但是由于自己是个菜鸡,所以这个方法并没有想出来,由思路的朋友可以留言给我点提示。
重新定义状态转移
下面说一种更简单的方法,不一定适用于所有题目,但是对这道题目确实适用,主要也是个大家一个思路,引导大家以后遇到类似问题的时候可以往这个方向思考。
dp(K,N)本身是表示当前状态为K个鸡蛋,面对N层楼,返回这个状态下最少的扔鸡蛋次数。
假设我们扔鸡蛋的次数为m,那么根据这个定义有dp[K][N] = m
我们之前的两种方法其实本质上都是穷举,遍历过程一定是穷举,这个没得说。而二分查找其实也只是对穷举的剪枝操作。我们的本质思路都是穷举。但是现在我换一种非穷举方法来解决这个问题!
我们稍微修改一下dp数组的定义,确定当前鸡蛋个数和最多允许的扔鸡蛋次数,就知道能够确定F的最高楼层数
!!!!!!这里请注意,我们不再是已知鸡蛋数和楼层数反推扔鸡蛋次数了,而是已知扔鸡蛋次数和鸡蛋数,去推楼层数了!!!!如果我们确定的最高楼层数达到了我们的N,我们也就得到了扔鸡蛋的最少次数。
dp[K][m] = N表示当前有K个鸡蛋,可以尝试扔m次鸡蛋,这个状态下,最坏情况下最多能确切测试一栋n层的楼。
比如说dp[1][7]表示我们有1个鸡蛋,可以扔7次,最多能检验几层楼,答案一定是7,我们只需要线性搜索即可。
我们将最初的最少扔鸡蛋次数改为现在的能检验的最高楼层数,当楼层数等于N时的扔鸡蛋次数就是我们的最少扔鸡蛋次数。这就是状态转移的重新定义。
其实我们状态只有两种,鸡蛋碎或者没碎:
1.如果鸡蛋碎了,我们就找楼下,如果鸡蛋没碎我们就找楼上
2.函数返回的是这个区间查找到的楼层数,所以总的层数一定=没碎的楼层数+碎的楼层数+1(第一次扔鸡蛋的楼层)
状态转移方程为:dp[k][m] = dp[k-1][m-1] + dp[k][m-1] + 1
其中如果鸡蛋没碎,则楼上的楼层数为dp[k][m-1]
如果鸡蛋碎了,则楼下的楼层数为dp[k-1][m-1]
最后加上我们最开始扔鸡蛋的1层,即可得到最终结果。

来看一下这个方法的代码:

int superEggDrop(int K,int N)
{//m最多不会超过N次(线性扫描)int[][] dp = new int[K+1][N+1];//base casedp[0][...] = 0dp[...][0] = 0int m = 0;while(dp[K][m] < N){m++;//穷举状态,鸡蛋个数for(int k = 1;k <= K;i++)dp[K][m] = dp[K-1][m-1] + dp[K][m-1] + 1;}return m;
}

当然啦,这个方法我们也可以写成二分法查找m的取值。

总结

第一个二分优化利用了dp函数的单调性,用二分查找快速搜索答案;第二种优化巧妙地修改了状态转移方程,简化了求解流程,但是确实难以想到。后续的数学方法就算了吧,
才疏学浅哈哈。
好啦,到这里我们的问题就说完了,这篇文章确实写的也比较细,因为细致写一篇文章确实能让自己收获不少,说不好也可以帮助别人。下次我们说另一个动态规划的经典问题——买卖股票问题。

动态规划之经典的鸡蛋坠楼问题详解相关推荐

  1. matlab中gad,10大经典算法matlab代码以及代码详解【数学建模、信号处理】

    [实例简介] 10大算法程序以及详细解释,包括模拟退火,禁忌搜索,遗传算法,神经网络.搜索算法. 图论. 遗传退火法.组合算法.免疫算法. 蒙特卡洛.灰色预测.动态规划等常用经典算法.是数学建模.信号 ...

  2. 常用经典SQL语句大全完整版--详解+实例 (存)

    常用经典SQL语句大全完整版--详解+实例 转 傻豆儿的博客 http://blog.sina.com.cn/shadou2012  http://blog.sina.com.cn/s/blog_84 ...

  3. Java经典面试题整理及答案详解(八)

    简介: Java经典面试题第八节来啦!本节面试题包含了进程.线程.Object类.虚拟内存等相关内容,希望大家多多练习,早日拿下心仪offer- 了解更多: Java经典面试题整理及答案详解(一) J ...

  4. Java经典面试题整理及答案详解(三)

    简介: 以下是某同学面试时,面试官问到的问题,关于面试题答案可以参考以下内容- 上一篇:Java经典面试题整理及答案详解(二) Java面试真题第三弹接住!相信通过前两节的学习,大家对于Java多少有 ...

  5. 十大经典排序算法-桶排序算法详解

    十大经典排序算法 十大经典排序算法-冒泡排序算法详解 十大经典排序算法-选择排序算法详解 十大经典排序算法-插入排序算法详解 十大经典排序算法-希尔排序算法详解 十大经典排序算法-快速排序算法详解 十 ...

  6. 十大经典排序算法-选择排序算法详解

    十大经典排序算法 十大经典排序算法-冒泡排序算法详解 十大经典排序算法-选择排序算法详解 十大经典排序算法-插入排序算法详解 十大经典排序算法-希尔排序算法详解 十大经典排序算法-快速排序算法详解 十 ...

  7. 十大经典排序算法-希尔排序算法详解

    十大经典排序算法 十大经典排序算法-冒泡排序算法详解 十大经典排序算法-选择排序算法详解 十大经典排序算法-插入排序算法详解 十大经典排序算法-希尔排序算法详解 十大经典排序算法-快速排序算法详解 十 ...

  8. 0x55. 动态规划 - 环形与后效性处理(例题详解 × 6)

    目录 0x55.1 环形结构上的动态规划问题 两次DP法 Problem A. Naptime 破环成链法 Problem B. 环路运输 Problem C. Two Rabbits 0x55.2 ...

  9. 动态规划——矩阵连乘问题算法及实现详解(附完整代码)

    问题分析 矩阵连乘问题是经典的动态规划问题,其主要是n个矩阵进行矩阵乘法运算时,通过括号改变运算的先后顺序,减少运算次数,找到最佳划分方法,求解最少运算次数. 算法分析 矩阵连乘问题中动态规划可以帮助 ...

最新文章

  1. Jupyter Notebook实现直接调用R
  2. Spring Cloud入门教程-Hystrix断路器实现容错和降级
  3. POJ 3216 Repairing Company【二分图最小路径覆盖】
  4. mysql 调试分析利器_使用systemtap调试工具分析MySQL的性能
  5. 最佳的七十五个网络分析和安全工具
  6. 查看自己设置的jvm参数
  7. origin 修改水平坐标的刻度
  8. 剑指offer之顺时针打印矩阵
  9. python函数参数定义顺序_18 Python - 函数定义与参数
  10. TeamTalk源码分析(1)
  11. opencv+VS2005安装说明
  12. Vscode新建vue模板
  13. FSR402电阻式薄膜压力传感器
  14. Vue :将头像/文本生成二维码
  15. 链改重塑信任,打造零风险的产业生态体系!
  16. 上海航芯|推出基于ACX200T的V2X解决方案
  17. 攻防世界 MISC新手练习区 刷12道题题所得的思路和方法
  18. 关于《后浪》的B站弹幕分析总结(四)——Python实现LDA内容主题挖掘及主题可视化
  19. Jav8 HashMap-putVal() 方法分析
  20. 手写喜马拉雅APP特效

热门文章

  1. 差分轮式机器人模型matlab,两轮差速机器人运动学分析和控制研究
  2. GooSeeKer集搜客工具爬虫入门
  3. 如何查找涉密文件_机关单位如何做好涉密文件、资料的使用
  4. java excel打印_技巧(excel,excel使用java打印输出)
  5. Acwing 平方矩阵 C++
  6. win10安装ubuntu18.04双系统,创建EFI分区
  7. 强化学习中状态价值函数和动作价值函数的理解
  8. ADS学习:元件库的安装与使用
  9. Electron桌面端开发(进程)
  10. 苹果手机中如何对CAD图纸进行缩小查看?