http://blog.csdn.net/joylnwang/article/details/6769160

经典的动态规划问题,题设是这样的:
如果你有2颗鸡蛋,和一栋36层高的楼,现在你想知道在哪一层楼之下,鸡蛋不会被摔碎,应该如何用最少的测试次数对于任何答案楼层都能够使问题得到解决。

  • 如果你从某一层楼扔下鸡蛋,它没有碎,则这个鸡蛋你可以继续用
  • 如果这个鸡蛋摔碎了,则你可以用来测试的鸡蛋减少一个
  • 所有鸡蛋的质量相同(都会在同一楼层以上摔碎)
  • 对于一个鸡蛋,如果其在楼层i扔下的时候摔碎了,对于任何不小于i的楼层,这个鸡蛋都会被摔碎
  • 如果在楼层i扔下的时候没有摔碎,则对于任何不大于i的楼层,这颗鸡蛋也不会摔碎
  • 从第1层扔下,鸡蛋不一定完好,从第36层扔下,鸡蛋也不一定会摔碎。

实际上,我们的终极目的是要找出连续的两层楼i,i+1在楼层i鸡蛋没有摔碎,在楼层i+1鸡蛋碎了,问题的关键之处在于,测试之前,你并不知道鸡蛋会在哪一层摔碎,你需要找到的是一种测试方案,这种测试方案,无论鸡蛋会在哪层被摔碎,都至多只需要m次测试,在所有这些测试方案中,m的值最小。

对于只有1颗鸡蛋的情况,我们别无选择,只能从1楼开始,逐层向上测试,直到第i层鸡蛋摔碎为止,这时我们知道,会让鸡蛋摔碎的楼层就是i(或者直到顶层,鸡蛋也没有被摔碎),其他的测试方案均不可行,因为如果第1次测试是在任何i>1的楼层扔下鸡蛋,如果鸡蛋碎了,你就无法确定,i-1层是否也会令鸡蛋摔碎。所以对于1颗鸡蛋而言,最坏的情况是使鸡蛋摔碎的楼层数i>=36,此时,我们需要测试每个楼层,总共36次,才能找到最终结果,所以1颗鸡蛋一定能解决36层楼问题的最少测试次数是36.

对于2个鸡蛋,36层楼的情况,你可能会考虑先在第18层扔一颗,如果这颗碎了,则你从第1层,到第17层,依次用第2颗鸡蛋测试,直到找出答案。如果第1颗鸡蛋没碎,则你依然可以用第1颗鸡蛋在27层进行测试,如果碎了,在第19~26层,用第2颗鸡蛋依次测试,如果没碎,则用第1颗鸡蛋在32层进行测试,……,如此进行(有点类似于二分查找)。这个解决方案的最坏情况出现在结果是第17/18层时,此时,你需要测试18次才能找到最终答案,所以该方案,解决36层楼问题的测试次数是18.

相较于1颗鸡蛋解决36层楼问题,测试次数实现了减半,但是18并不是确保解决2颗鸡蛋,36层楼问题的最小值(实际的最小值是8).

我们可以将这样的问题简记为W(n,k),其中n代表可用于测试的鸡蛋数,k代表被测试的楼层数。对于问题W(2,36)我们可以如此考虑,将第1颗鸡蛋,在第i层扔下(i可以为1~k的任意值),如果碎了,则我们需要用第2颗鸡蛋,解决从第1层到第i-1层楼的子问题W(1,i-1),如果这颗鸡蛋没碎,则我们需要用这两颗鸡蛋,解决从i+1层到第36层的子问题W(2,36-i),解决这两个问题,可以分别得到一个尝试次数p,q,我们取这两个次数中的较大者(假设是p),与第1次在i层执行测试的这1次相加,则p+1就是第一次将鸡蛋仍在i层来解决W(2,36)所需的最少测试次数次数ti。对于36层楼的问题,第一次,我们可以把鸡蛋仍在36层中的任何一层,所以可以得到36中解决方案的测试次数T{t1,t2,t3,……,t36},在这些结果中,我们选取最小的ti,使得对于集合T中任意的值tj(1<=j<=36,j!=i),都有ti<=tj,则ti就是这个问题的答案。用公式来描述就是W(n, k) = 1 + min{max(W(n -1, x -1), W(n, k - x))}, x in {2, 3, ……,k},其中x是第一次的测试的楼层位置。

其中W(1,k) = k(相当于1颗鸡蛋测试k层楼问题),W(0,k) = 0,W(n, 0) = 0

所以在计算W(2,36)之前,我们需先计算出所有W(1,0),……,W(1,36),W(2,0),……,W(2,35)这些的值,可以用递推的方法实现,代码如下:

  1. unsigned int DroppingEggsPuzzle(unsigned int eggs, unsigned int floors)
  2. {
  3. unsigned int i, j, k, t, max;
  4. unsigned int temp[eggs + 1][floors + 1];
  5. for(i = 0; i < floors + 1; ++i)
  6. {
  7. temp[0][i] = 0;
  8. temp[1][i] = i;
  9. }
  10. for(i = 2; i < eggs + 1; ++i)
  11. {
  12. temp[i][0] = 0;
  13. temp[i][1] = 1;
  14. }
  15. for(i = 2; i < eggs + 1; ++i)
  16. {
  17. for(j = 2; j < floors + 1; ++j)
  18. {
  19. for(k = 1, max = UINT_MAX; k < j; ++k)
  20. {
  21. t = temp[i][j - k] > temp[i - 1][k -1] ?  temp[i][j - k] : temp[i - 1][k -1];
  22. if(max > t)
  23. {
  24. max = t;
  25. }
  26. }
  27. temp[i][j] = max + 1;
  28. }
  29. }
  30. return temp[eggs][floors];
  31. }

算法的空间复杂度是O(nk),时间复杂度是O(nk^2),对于规模较大的问题,无论是空间还是时间复杂度都很可观。

这个算法可以计算出W(2,36)问题的最少测试次数是8,但是却不能给出用2颗鸡蛋解决36层楼问题的具体方案,这里我就给出一个测试方案:

  • 用第一颗鸡蛋分别在8,15,21,26,30,33,35层进行测试
  • 如果鸡蛋在某一层碎了(例如26层),则在前一测试点由下到上依次测试,例如(22,23,24,25),直到找到满足条件的楼层为止
  • 如果鸡蛋在第35层的测试中也没碎,则用该鸡蛋在第36层再测试一次

该方案可以保证,无论满足条件的楼层是多少,都可以在最多8次测试之后找到答案,例如目标楼层为28时,该方案的测试顺序为8,15,21,26,30,27,28,总共测试7次,有兴趣的读者可以尝试一下其他情况。

该方案解决W(2,36)问题比较优雅,但是却暗藏一个很大的玄机,那就是一般我们见到的这个问题的题面,往往是W(2,15),W(2,36),不知道读者考虑过没有,为什么非让我们计算2颗鸡蛋测试36层楼的情况,而不是35层或者37层?下面是用之前的算法解决W(4,50)问题的递推结果表格(其中,行代表楼层数1~50,列代表鸡蛋数1~4),我们会发现,W(2,36)=8,W(2,37) = 9,那么是不是用2颗鸡蛋测试8次,最多只能解决36层楼问题,对于37层就无能为力了呢?

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
1 2 2 3 3 3 4 4 4 4 5 5 5 5 5 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10
1 2 2 3 3 3 3 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7
1 2 2 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6

这里引出了一个问题:n个鸡蛋,测试m次(简记为D(n,m)),最大可以解决几层楼的问题,通过对递推结果表格的观察,我们可以得到如下结论

  1. D(1,m) = m
  2. D(n,n) = 2^n - 1
  3. D(n,m){m <= n} = D(m,m)

对于第二点,以D(4,4)为例,我们第1次在8楼扔下鸡蛋,如果碎了,则第二次在4楼扔下鸡蛋,否则在12楼扔下鸡蛋,对于在4楼扔下鸡蛋的情况,之后可以分别在2楼或者6楼扔下鸡蛋,如此进行,就可以找到答案楼层,方法与二分查找一样。例如答案楼层是5的情况,测试序列为8,4,6,5。

对于第三点,如果有5个鸡蛋让你测试3次,即使三次测试鸡蛋都碎了,剩下的2个鸡蛋也派不上用场,所以D(5,3) = D(3,3)

发现这些关系之后,我们似乎找到解决n个鸡蛋测试m次最大能够解决楼层数的方法。对于D(n,m){n < m}而言,对于其能够测试的最大楼层数k,我们可以构造这样的场景,将第一颗鸡蛋仍在楼层i,使得第i + 1层到第k层是D(n,m-1)可以解决的最大楼层数,第1层到第i - 1层是D(n-1,m-1)可以解决的最大楼层数,由此得到递推关系D(n,m) = D(n -1,m-1) + 1 + D(n,m-1),然后对D(n,m-1),D(n-1,m-1)再按照上述公式分解,直到得出刚才所列的三种可计算情况(n = 1,或者m <= n)为止,再进行回溯累加,就可以得到D(n,m)的值,代码如下:

  1. unsigned int DroppingMax(unsigned int eggs, unsigned times)
  2. {
  3. if(eggs == 1)
  4. {
  5. return times;
  6. }
  7. if(eggs >= times)
  8. {
  9. return (unsigned int)pow(2, times) - 1;
  10. }
  11. return DroppingMax(eggs, times -1) + DroppingMax(eggs -1, times - 1) + 1;
  12. }

根据此算法,我们可以得出D(2,5)=15,D(2,8)=36,也就是说,2个鸡蛋测试5次最多可以解决15层楼的问题,测试8次最多可以解决36层楼的问题。可见,出这个题的人并不是随便找两个楼层数陪咱们玩玩,而是对此问题认真研读后的结果。有了此利器之后,我们解决扔鸡蛋问题的的方法将得到大幅简化,对于n个鸡蛋解决k层楼的问题我们只需找到这样的值m,使得D(n,m-1)<k<=D(n,m),代码如下

  1. unsigned int DroppingEggsPuzzle2(unsigned int eggs, unsigned int floors)
  2. {
  3. unsigned int times = 1;
  4. while(DroppingMax(eggs, times) < floors)
  5. {
  6. ++times;
  7. }
  8. return times;
  9. }

该算法的时间和空间复杂度不太好分析,但都要好于传统的DP算法,有兴趣的读者可以推敲一下,在我的机器上测试10个鸡蛋,5000层楼的情况,第二个方法比第一个要快10万倍!注意到算法2也是一个动态规划问题,所以可以用一个n*m的矩阵来保存计算过程中的中间结果,算法的效率还可以得到很大提升!

不管是算法1,还是算法2,都没有给出用n个鸡蛋如何通过m次测试,解决k层楼的问题,对此我根据算法2给出一个思路。对于满足条件D(n,m-1)<k<=D(n,m),的测试次数m,将D(n,m),和D(n,m-1)按照D(n,m) = D(n -1,m-1) + 1 + D(n,m-1) 的方式展开,这里展开过程中要严格按照公式中各迭代的顺序,也就是说先是D(n-1,m-1),然后是1,然后是D(n,m-1),顺序不能乱,然后比较两结果,例如

D(3,5) = D(1,3)+1[2]+D(1,2)+1[3]+D(2,2)+1[1]+D(1,2)+1[3]+D(2,2)+1[2]+D(3,3)
D(3,4) =                                     D(1,2)+1[2]+D(2,2)+1[1]+D(3,3)

这其中每个单独的1,都代表一次独立测试,这些1后面中的中括号代表其是第几次独立测试,与其从公式中分离出来的时机相关,最早分离出来的1,其值就是[1],第二次分离出来的1,其值就是[2],这些1的目的就是把k层楼分解为若干个可直接计算的子部分。我们取出两者不同的部分D(1,3)+1[2]+D(1,2)+1[3]+D(2,2)+1[1],这部分表示通过增加了一次测试,我们所获得的额外的探测能力,通过改造这部,使得这部分的和等于k-D(n,m-1),然后将改装部分与两者的相同部分结合,形成新的结果,这些结果从前到后,对应着楼层从下到上的测试方案

上例中我们知道D(3,4)=14, D(3,5)=25,对于14 < k <= 25,我们用k减去14得到需要构造的值,尽量保留右侧的算式,只改变最左侧的算式,例如对于k = 15,不同部分可以用1替换,对于k = 16可以用D(1,1)+1替换,对于k = 18可以用D(2,2)+1替换,对于k = 21可以用D(1,2)+1+D(2,2)+1替换。以21为例,我们将改造结果和D(3,4),D(3,5)的相同部分结合,形成

D(1,2)+1[2]+D(2,2)+1[1]+D(1,2)+1[3]+D(2,2)+1[2]+D(3,3)
下面用图说明如何用3个鸡蛋测试5次,解决21层楼问题,这里的规律是,对于独立的测试而言,如果测试摔碎,则向低楼层执行后续的测试,如果没有摔碎,则向高楼层执行后续的测试,其中的括号表示该测试执行的楼层/楼层区间。

实际上,对于D(n,m-1)<k<D(n,m)的情况,满足条件的测试方案不止一种。
后记:

    • 这是国外牛人的一篇文章,对于扔鸡蛋问题的理论分析,让人叹服,有兴趣的读者可以看一看,进一步深挖这个问题
    • 对于算法2的几个前提,我没能给出数学上的证明,那篇国外大牛的文章里面有涉及,不过那篇文章太长了,我没有看完。
    • 根据D(n,m)的递推关系,也许可以得到这个数列的通项公式。
    • 扔鸡蛋问题,与其说是一个动态规划问题,不如说是一个在特定场景下的数学问题,程序在该问题中更多价值在于验证结论。

扔鸡蛋问题详解(Egg Dropping Puzzle)的更多相关文章

  1. 扔鸡蛋问题具体解释(Egg Dropping Puzzle)

    经典的动态规划问题,题设是这种: 假设你有2颗鸡蛋,和一栋36层高的楼,如今你想知道在哪一层楼之下,鸡蛋不会被摔碎,应该怎样用最少的測试次数对于不论什么答案楼层都可以使问题得到解决. 假设你从某一层楼 ...

  2. Egg Dropping Puzzle问题的分析

    首先,基本问题是这样:You are given two eggs, and access to a 100-storey building. The aim is to find out the h ...

  3. Egg Dropping Puzzle

    The Two Egg Problem 曾经是Google的一道经典题. 题意:有一个百层高楼,鸡蛋在\(L\)层及以下扔都不碎,在\(L\)层以上都会碎.现在某人有\(k\)个鸡蛋,问在最坏情况下, ...

  4. 动态规划法(六)鸡蛋掉落问题(一)(egg dropping problem)

      继续讲故事~~   这天,丁丁正走在路上,欣赏着路边迷人的城市风景,突然发现前面的大楼前围了一波吃瓜群众.他好奇地凑上前去,想一探究竟,看看到底发生了什么事情.   原来本市的一位小有名气的科学家 ...

  5. Coursera Algorithms week1 算法分析 练习测验: Egg drop 扔鸡蛋问题

    题目原文: Suppose that you have an n-story building (with floors 1 through n) and plenty of eggs. An egg ...

  6. Leetcode 887 Super Egg Drop(扔鸡蛋) DP

    这是经典的扔鸡蛋的题目. 同事说以前在uva上见过,不过是扔气球.题意如下: 题意: 你有K个鸡蛋,在一栋N层高的建筑上,被要求测试鸡蛋最少在哪一层正好被摔坏. 你只能用没摔坏的鸡蛋测试.如果一个鸡蛋 ...

  7. Java 虚拟机详解

    深入理解JVM 1   Java技术与Java虚拟机 说起Java,人们首先想到的是Java编程语言,然而事实上,Java是一种技术,它由四方面组成: Java编程语言.Java类文件格式.Java虚 ...

  8. (十)Maven依赖详解

    1.何为依赖? 比如你是个男的,你要生孩子,呸呸呸...男的怎么生孩子,所以你得依赖你老婆,不过也不一定咯,你也可以依赖其她妹子. 我们在平时的项目开发中也是同理,你需要依赖一些东西才能实现相应的功能 ...

  9. Redis快速入门详解

    Redis入门详解 Redis简介 Redis安装 Redis配置 Redis数据类型 Redis功能 持久化 主从复制 事务支持 发布订阅 管道 虚拟内存 Redis性能 Redis部署 Redis ...

随机推荐

  1. 最受欢迎的Web开发工具

    1)WebBuilderWebBuilder是一款开源的可视化Web应用开发和运行平台.基于浏览器的集成开发环境,可视化和智能化的设计,能轻松完成常规应用和面向手机的移动应用开发:高效.稳定和可扩展的 ...

  2. LigerUI权限系统之用户管理

    用户管理较之前的的组织结构和菜单管理稍显复杂.不管怎样还是先上图吧,再来讲解 左边是组织结构,右边是用户,用户是跟组织机构挂钩的,通过点击左边的组织结构,来刷新右边,加载该组织机构下的用户. 用户管理 ...

  3. spring mvc后台接收中文乱码

    可从如下几方面着手 1.jsp页面编码 2.web.xml配置字符过滤器,该字符过滤器最好放在开头 3.tomcat下server.xml添加URIEncoding="UTF-8" ...

  4. .Net程序员学用Oracle系列(4):四个基本概念

    <.Net程序员学用Oracle系列:导航目录> 本文大纲 1.概念说明 1.1.表空间 1.2.SCHEMA 1.3.用户 1.4.权限 2.表空间管理 2.1.创建表空间 2.2.监控 ...

  5. Maven3在Eclipse上安装插件

    eclipse 安装插件的方式最常见的有两种: 1. 一种是在线安装,这貌似是用的最多的,就是:Help -->  Install New Software,然后输入 HTTP 地址来安装,但有 ...

  6. RocketMQ源码 — 一、 quikstart

    RocketMQ quikstart 获取源码 git clone https://github.com/alibaba/RocketMQ.git 编译打包 cd RocketMQ bash inst ...

  7. Symfony3 更改生成CRUD目录步骤

    ---恢复内容开始--- 今天是2017-01-16号,项目零零散散的进行着: 由于Symfony3生成的crud在app目录需要转移到相应的Budle下面,记录以下过程: 我使用的是Symfony3 ...

  8. 【.NET-EF】Entity Framework学习笔记2 - 增删改(没查询)

    学习描述:用EF就像是省略了做实体类和DAL类,感觉是很方便,废话不多说,直接写步骤: 1.创建EF的edmx文件 这个其实在笔记1已说过,不过有些细节也要说,所以再说一遍,这里使用的是EF 6.1版 ...

  9. Kattis - Biased Standings

    Biased Standings Usually, results of competitions are based on the scores of participants. However, ...

  10. D3.js:力导向图

    var nodes = [ { name: "桂林" }, { name: "广州" }, { name: "厦门" }, { name: ...