鸡蛋掉落问题详解
鸡蛋掉落问题指的是在允许有限次出现错误状态的情况下,寻找当前问题解决方法。一个游戏类型的版本为:在一个高为n层的塔上,你有m个完全相同的理想鸡蛋可以扔下去,这些鸡蛋都具有一个属性就是,存在一个楼层高度f,当你从低于f的楼层扔下去时,鸡蛋不会破碎;当从高于或等于f的楼层扔下时,鸡蛋会碎掉,碎掉的鸡蛋当然不能再用。你要做的是:无论f为多少(0<=f<=n),为了明确的确定出其值至少需要做多少次尝试?(注意,这里明确的确定指的是,无论f的值是多少,在你给的尝试次数限定下,都可以找到至少一种方案确定出它的值。如果少做一次尝试,就无法对某些f的值无法验证)这个问题有很多现实应用版本,例如,规避访问那些慢速的硬盘、将缓存未命中次数降到最低或者在一个数据库上做多次昂贵的查询。
目录
- 2个鸡蛋,100层楼; (m=2,n=100)
- 2个鸡蛋,k层楼; (m=2,n=k)
- N个鸡蛋,k层楼; (m=N,n=k)
- 递归解法
- 动态规划解法(DP 算法)
- 二项式表达
一、2个鸡蛋,100层楼
你有2个鸡蛋,要在100层的楼上确定出f:
- 如果鸡蛋在某个楼层(x)没有碎,则对任何低于x的楼层都不会碎;
- 如果鸡蛋在某个楼层(x)碎了,则对于任何高于x的楼层也都会碎;
- 鸡蛋可能在第一层楼就碎了;
- 鸡蛋可能在最顶层楼也不会碎。
一开始你可能会想到使用二分法解决这个问题(我就是),但那绝不是最好的策略。不妨想一想,你有两个鸡蛋,碎了一个还有一个,即使没碎也意味着你已经进行了一次尝试,最开始的一次尝试应该选在哪个楼层呢?
如果使用二分法,你最开始会尝试第50层楼,如果没碎,再尝试75层楼,如果鸡蛋一直没碎,你做7次才能确定。如果鸡蛋在50层(或者随便哪一层)碎了,你必须使用唯一剩下的鸡蛋,一层一层的往下尝试,完全有可能是49次,因此这种策略复杂度为O(n);
我们接下来将证明,第一次尝试的最佳楼层为14,无论f的真实值为多少,第一次尝试都应该选在14层。
方案为:如果在14层碎了,你接下来的路线为:1-2-3-…13,共需要14步;
如果在14层没有碎,你需要尝试27层,我们可以将方案用图表展示:
这表明:无论f为多少,我们最多尝试14次就可以确定出其位置。
二、2个鸡蛋,k层楼
我们利用两个鸡蛋在k层楼进行尝试,一个很好的思考方向是:我们在x的尝试次数下,能否覆盖所有楼层?(这里的覆盖的意思是,如果f在这个楼的任意一层,那我们最多尝试x次,一定能确定出其位置,例如在上一种情况,如果我们有14次尝试机会,我们肯定可以覆盖100层的楼)。
我们假定现在有一个最好的方案,在这个方案中,最大尝试次数(也就是是f在最难找的层数,如上例的13、26、38等层数)是x (上例中的14),那么我们需要从x层做第一次尝试:
如果第一次碎了:你需要从1、2、3、x-2、x-1层依次尝试(上例的第一种情况),总次数为:((x-1)-1+1)+1= x次;总覆盖为:1— x = x层。
如果第一次没碎:你需要在第(x+(x-1))层做第二次尝试;
如果第二次碎了: 你需要从第x+1、x+2、…x+(x-1)-2、x+(x-1)-1层依次尝试(上例的第二种情况),总次数为((x+(x-1)-1)-(x+1)+1)+2=x次;总覆盖为:x+1 — x+(x-1)-1 = x-1 次;
如果第二次没碎: 你需要从第(x+(x-1))+(x-2)=3x-3层做第三次尝试;
如果第三次碎了: 同理;以此类推,直到第x次尝试。
意识到我们正在做什么了吗?我们现在不是在针对具体楼层寻找最小的尝试次数,而是现在将问题转化为:如果我们可以尝试x次,那么我们最高可以到哪一层?
我们可以这样总结:在2个鸡蛋的情况下,我们在x的尝试范围内,可以覆盖到:
三、N个鸡蛋,k层楼
通过上面的情况我们知道:必须找到一种关于鸡蛋数和楼层数的通用的算法;
* 递归版本:这种最好理解,也最好实施,但是最慢的一种,不建议使用。
* 动态规划版本:与递归类似,但是更快,适用于中小型问题。
* 二分——递归结合:最快,如果明白策略也很好实施。
四、 递归解法
假设你有n个鸡蛋和h个待测试的楼层,现在你从第i层扔下,有两种结果:
* 碎了:问题变成了:你有n-1个鸡蛋,i-1个待测楼层(1、2、…i-1)。
* 没碎: 问题变成了:你有n个鸡蛋,h-i个待测楼层(i+1,i+2,…h).
你应该意识到:重要的不是我们总共要探测的楼层数,而是那些仍有嫌疑的楼层数。例如,我们去检查(1-20)层和(21-40)层是没有区别的,我们一共检查了二十层。
现在我们可以用函数W(n,h) 表示最优次数:
边界条件为:
1个鸡蛋,h层楼: h次;
n个鸡蛋,1层楼: 1次;
n个鸡蛋,0层楼: 0次。
C++实现
#include <iostream>
#include <limits.h>
using namespace std;
//Compares 2 values and returns the bigger one
int max(int a,int b) {
int ans=(a>b)?a:b;
return ans;
}
//Compares 2 values and returns the smaller one
int min(int a,int b){
int ans=(a<b)?a:b;
return ans;
}
int egg(int n,int h){
//Basis case
if(n==1) return h;
if(h==0) return 0;
if(h==1) return 1;
int minimum=INT_MAX;
//Recursion to find egg(n,k). The loop iterates i: 1,2,3,...h
for(int x=1;x<=h;x++) minimum=min(minimum,(1+max(egg(n,h-x),egg(n-1,x-1))));
return minimum;
}
int main()
{
int e;//Number of eggs
int f;//Number of floors
cout<<"Egg dropping puzzle\n\nNumber of eggs:";
cin>>e;
cout<<"\nNumber of floors:";
cin>>f;
cout<<"\nNumber of drops in the worst case:"<<egg(e,f);
return 0;
}
五,动态规划
C++实现
int solvepuzzle(int n,int k){
int numdrops[n+1][k+1];
int i,j,x;
for(i=0;i<=k;i++) numdrops[0][i]=0;
for(i=0;i<=k;i++) numdrops[1][i]=i;
for(j=0;j<=n;j++) numdrops[j][0]=0;
//This loop fills up the matrix
for(i=2;i<=n;i++){
for(j=1;j<=k;j++){
//Defines the minimum as the highest possible value
int minimum=INT_MAX;
//Evaluates 1+min{max(numeggs[i][j-x],numeggs[i-1][x-1])), for x:1,2,3...j-1,j}
for(x=1;x<=j;x++) minimum=min(minimum,(1+max(numdrops[i][j-x],numdrops[i-1][x-1])));
//Defines the minimum value for numeggs[i][j]
numdrops[i][j]=minimum;
}
}
cout<<"\nArray:\n\n";
//Prints numeggs
for(i=0;i<=n;i++){
for(j=0;j<=k;j++){
cout<<numdrops[i][j]<<" ";
}
cout<<"\n";
}
cout<<"\nNumber of trials in the worst case using the best strategy:\n";
return numdrops[n][k];
}
六、二项—递归解法
在开始之前,先看一点数学知识:
二项分布:
杨辉三角
将杨辉三角写成如下形式:
可以找到如下递归式:
现在,我们开始下一种解法:
第一步:我们可以将问题重新表述:如果我们有d次尝试机会,n个鸡蛋,我们最大可以覆盖的层数为f(d,n):
有:
(1)
如果我们能找到这样一个函数f(d,n),问题就迎刃而解了。下面将介绍如何寻找到这个函数:
假设一个辅函数:
(2)
代入方程1可得:
可以看到,这个辅函数的迭代式和前面杨辉三角递推式完全相同。因此可以将g(d,n)写成如下形式:
但是这里需要做一点修正,因为对任意n,f(0,n)=0;所以g(0,n)也应该为0,但是
我们可以做如下修正:
这样对迭代式依然满足。
接下来有:
因为f(d,0)=0;结合(2)式:
这样,求解尝试次数就变成了: