最坏情况下求得最优解所需的次数
内容说明
本文是在看过<<妙解谷歌压箱底面试题:如何正确的从楼上抛鸡蛋>>一文以后做的总结,该文章对此问题描写的很详细,但是在拜读的过程中也花了一些时间去理解和消化,所以在读完以后按照自己的理解写了这篇文章,用以梳理思路并验证。所有思路均非原创
##问题描述
问题简述:有两个完全相同的鸡蛋,从100层高的楼往下扔,要求找出最坏情况下在保证鸡蛋不破的前提下找到最高的楼层且计算扔鸡蛋的次数最少
问题分析
首先,在我的理解下这是两个个最优解问题,第一,在保证楼层最高的情况下计算次数最少;第二,在保证鸡蛋不破的前提下楼层最高
第二个问题很容易解,很直观的思路,从一层开始一层一层往上,最终能够得到一个最高楼层;但是这对于第一个问题就变得很不友好,在最坏情况下我需要扔100次才能找到最终楼层
不过第一个问题的思路也很直观,分区,要想扔的次数少肯定不能一楼一楼的往上,那就要跳层,好在两个鸡蛋,如果跳层成功则继续跳,失败了就在这个区间内一层一层往上
于是这个问题就变成了怎么跳层即分区的问题,一般常用的分区是等分法,但显然这个问题并不适用,在最坏的情况下,分区越靠后扔鸡蛋的次数就越多,不符合这一个问题的要求
于是想到另一种常见的分区方式:每次跳转剩余楼层的1/n楼,如果最高楼层在前面的区间,多层跳转(从m层跳转到m+n层,n大于1)的次数少,但因为区间大,所需的单层跳转(从m层跳转到m+1层)次数也多;如果最高楼层在后面的区间,跳转的次数多了,但是单层跳转次数却变少了。这样无论最高楼层在哪里,都能有一个相对比较平衡的跳转次数,这种分区方式很好的较少了最坏情况下的平均跳转次数,但是并不能保证任何情况下跳转次数都是最少的
那么如果保证无论最后的楼层在哪一个区间跳转次数都最少,就必须保证每一个区间的跳转次数都是相等的。(n个数的和是固定的,想要n个数都最小,只能让n个数相等)
既然最简单的等分不适合了,那么我们就需要去计算一个可靠的分区方式。先做一些假设方便计算:
- 相同的楼层对任意鸡蛋都是同样的效果
- 鸡蛋破碎就不能继续使用
- 鸡蛋完好则可以继续使用
- 鸡蛋在第n层没有破,那么在小于n层的楼层也不会破
- 鸡蛋在第n层破了,那么在大于n层的楼层也会破
- n为大于0的正整数
这个问题里面包含几个变量:f(i)表示在最坏时需要扔鸡蛋的次数最少的情况下第i次扔鸡蛋的楼层(最优解第i次扔鸡蛋所在的楼层),W(i)表示如果第i层是鸡蛋不破的最高层,那么最少需要的扔鸡蛋的次数为W(i)
显然f(0)=0(第0次还没有开始扔),f(N)=100-1=99(最后一次扔鸡蛋一定是在100楼,所以上一次一定是在100层的前一层即99层)。f(i)将100层分成了N+1个区间
- { [1,f(1)), [f(1),f(2)), …, [f(N),100,…) }
假设鸡蛋不破的最高楼层在第1个区间,那么W(1)=f(1),此时的场景是:第一个鸡蛋从f(1)层往下扔,破了,第二个鸡蛋从第1层开始一层一层往上,最坏的情况需要扔f(1)-1次,所以共需扔f(1)次
依次类推,假设鸡蛋不破的楼层在第j个区间,此时W(j)=j+f(j)-f(j-1)-1,假设f(0)=0,那么
- ** W(N) = f(N) - f(N-1) - 1 + N,N > 0**
理论上我们希望W(N)是一个常量数列,那么
- 因为W(j+1) = W(j)
- 所以f(j+1) + f(j-1) + 1 = 2f(j), 即f(j+1)-f(j)=f(j)-f(j-1)-1
g(n)=f(n)-f(n-1)是一个等差数列,此时我们知道已经没有办法更加简化了,需要的是进一步的计算
代码
根据上述分析,可以得到非常简单的实现:
//这里的iStartFloor和iEndFloor分别为1和100
void calFloors(int iStartFloor,int iEndFloor){
cout<<"calFloors starts, the result is:"<<endl;
int* a= new int[iEndFloor - iStartFloor +1];
//不妨假设最高楼层在最后一个区间
//只剩下一个鸡蛋,最后一步一定是最后一层
//上一步一定是最后一层的前一层
a[0] = iEndFloor;
a[1] = iEndFloor - 1;
int i=2;
while (2*a[i-1]-a[i-2]-1 >= iStartFloor){
a[i] = 2*a[i-1] - a[i-2] - 1;
i++;
}
for (int j = 0; j < i; j++){
cout<<a[j]<<",";
}
cout<<endl;
delete[] a;
}
输出的结果如下:
根据传统的遍历法计算得到的结果与上述一直,遍历的代码如下(该遍历方式未经过任何优化,计算速度相当堪忧,100楼跑程序跑的我都吐了,网上有很多优化遍历的算法,有兴趣可以自行搜索,在100楼的情况下相对原生遍历计算速度大大提高,这里就不给出具体实现了):
//投掷次数的最优解:假设每一个下一层决策都是最优的,那么整体结果就是最优的
int bestMaxThrows(int iFloorsLeft){
return maxThrows(iFloorsLeft, bestNextStep(iFloorsLeft));
}
//下一层的最优解
int bestNextStep(int iFloorsLeft){
if (iFloorsLeft <= 2)
return 1;
else{
int minThrows = iFloorsLeft;
for(int i = 1; i < iFloorsLeft; i++){
int tmpThrows = maxThrows(iFloorsLeft,i);
if (minThrows > tmpThrows)
minThrows = tmpThrows;
}
return minThrows;
}
}
//计算投掷次数
int maxThrows(int iFloorsLeft, int iNextFloor){
if (iFloorsLeft <= 2)
return iFloorsLeft;
else
return max(iNextFloor,bestMaxThrows(iFloorsLeft - iNextFloor) + 1);
}
//输出每次投掷的楼层
void printBestFloors(int iFloors){
cout<<"遍历计算的结果是:";
int floor = 0;
while(floor < iFloors){
int iFloorLeft = iFloors - floor;
int nextStep = bestNextStep(iFloorLeft);
floor += nextStep;
cout<<floor<<",";
}
cout <<endl;
}
问题扩展
到目前为止,该问题对于两个鸡蛋在任何有限的高楼里都能获得理想的解决方案,需要考虑的是如果鸡蛋数目是n个,该方案是否有效。
事实上,如果把每一个分区都当做一个待划分的区域,那么鸡蛋的个数越多也就是在某一个分区内自己做的分区越多。知道只剩下最后一个鸡蛋,就按照一层一层向上,即分区到达最小粒度。
举个栗子,如果我有3个鸡蛋,还是划分100层,那么按照上面两个鸡蛋时的划分方法,先跳层(后面叫做外层分区),在确定分区以后用掉了一个鸡蛋,然后在这个分区里再做上述划分的过程,用完剩下两个鸡蛋确认最后的楼层(内层分区),初步设想最终最坏情况下的最少次数应该是少于2个鸡蛋的。
那么这种方式是否可以用于2个以上的鸡蛋呢,这种方式的计算基础就是基于只有两个鸡蛋,现在不止两个鸡蛋了,这种方式是否依然能够找到正确的结果呢。
从直觉来看,应该是适用的。首先内层分区上面已经验证过是有效地,然后在最坏的情况下,也需要外层分区按这个逻辑走。验证待补充