该题由于太过于经典,现在已经不是Google的面试题了

思路讲解
大家可能存在的疑问解答
例题:LeetCode 887 鸡蛋掉落
PS:(蓝桥杯摔手机就是根据扔鸡蛋过来的)

经典面试题扔鸡蛋(Google面试题附带LeetCode例题)_Google

 

思路讲解

你有两个鸡蛋,在一百层的楼上,尽可能少的尝试次数可以找出在那一层掉落而不碎

吐个槽先

正如昨天所说,这道题据说它最早见于谷歌的某次面试,由于题目表述容易,而解答相对麻烦,于是被很多人采用,广泛见于一些算法、规划的面试里。

要说清楚这个问题有点难度,有兴趣的小伙伴务必要静下心来仔细看。

首先我想说,该问题的情境构造是有缺陷的。因为在现实中,影响鸡蛋是否会破裂的最重要因素应该是地面的坚硬和平整程度,而不是鸡蛋下落的高度:把鸡蛋从100层楼顶扔进棉花堆,和从2楼扔向坚硬的水泥地面:依常识判断,还是后者更容易碎。就像那句话说的,能够杀死你的是突然的速度改变,而不是高速本身:你会在接触地面的一瞬间死去,而不是死于高速坠落。

不过你懂的,这是题目,认真你就输了。

我们只需要遵循题目本身的设定就好,吐槽可以,不改变事实。

一个鸡蛋的无助

为了更好地解释这个问题,让我们先看一个简化的情况:如果你手头只有一个鸡蛋,该如何找到这个最高安全楼层?

——显然只能一层层地试。

简单地说,这个过程就是先在1楼把鸡蛋扔下地,看看碎不碎。

碎了,那么得到最高安全楼层为0。

如果没碎,那么拾起这个鸡蛋,上2楼,再扔下地,看碎不碎。

同样,碎了,那么得到最高安全楼层为1;没碎,那就再拾起鸡蛋奔3楼……

重复这个过程,直到发生两个结果之一:鸡蛋在第N层上首次碎掉,那么得到最高安全楼层为N-1;或者鸡蛋一直没碎,最高安全楼层为100(其实应该大于100,但这道题默认最大是100,所以姑且认为是100)。

由于鸡蛋随时可能会在某一层突然碎掉,而且一旦碎掉就再也没有鸡蛋可用,所以在整个过程中不能有任何投机取巧,只能逐层尝试,否则在任何一层上蛋碎了,我们都不能准确得出最高安全楼层。

一个鸡蛋的情况大致如此。虽然答案简单粗暴,但涉及的思路会对两个鸡蛋的情况有所启示。

两个鸡蛋的闹腾

两个鸡蛋的情况下,基本的想法也不难理解:

由于有两个鸡蛋可以用,所以第一个鸡蛋得用来确定一个比较大的范围。

但是心太黑了也不合适,步子迈得太大容易扯着蛋。

比如你一下子跑到50楼扔鸡蛋,看上去确实大量减少了工作量,但是蛋碎了怎么办呢?如果第一个蛋碎了,这时可以使用的蛋就只剩一个,而刚刚说了,只有一个蛋时我们没有选择,只能逐层去试。

这里体现了切分范围的思想,但同时体现了一个重要事实:
鸡蛋碎了比没碎要糟糕。

正因为这个事实的存在,因此在1-100层楼之前,我们第一个蛋的首次尝试应该向1这边(较可能不碎)倾斜,而不是向100那边(较可能碎)。

这样一个思路就出炉了:把100分成10个10层,第一个鸡蛋用来确认在哪个10层里,第二个鸡蛋用来确认具体层数。

具体来说就是,拿着第一个鸡蛋从第10层尝试,只要没碎就再上10层,直至碎了或者爬到楼顶,这样确定了十位数的范围。然后再用第二个鸡蛋逐层尝试。

比如第一个鸡蛋:

第10层扔下没碎,

第20层扔下没碎,

第30层扔下没碎,

第40层扔下碎了;

那么就拿第二个鸡蛋从31层至39层开始依次逐层尝试,直至排查出最高安全楼层为止。

这个思路已经基本成型,也比较接近答案了,只需要最后一点调整。

最后的雕琢

上面这个10*10的思路比逐层尝试已经有显著改进。

它的基本思路是,将100层切分成两个维度,由两个鸡蛋分别控制一个维度。

换言之,是将100层切分成若干个区块,由第一个鸡蛋确定最高安全楼层所属的区块,再由第二个鸡蛋逐层确定其具体的位置。大致思路如下:

每十个楼层,算作一个区块的话

 1-10         —— 区块1
 11-20        —— 区块2
 21-30        —— 区块3
   ……
 

但仔细思考的小伙伴可以发现,这个答案还有问题。

比如如果鸡蛋最高安全楼层为16或者96,用刚刚那个思路的话,这两种情况的总尝试次数并不一样:最高安全楼层为16时,第一个鸡蛋试了2次就定位了区块;而最高安全楼层为96时,第一个鸡蛋试了10次才定位了区块。虽然在区块内部的第二个鸡蛋的逐层尝试是一样的,但96层对应的总尝试次数就多得太多了。

原因就是10*10的区块均匀划分对大数不利。

因为碎和不碎这两种状态是不对称的,所以第一个鸡蛋的尝试的过程只能从小数逐渐尝试到大数,而不能反着来。所以均匀划分区块对大数是不公平:同样在每个区块里的第6层,第一个鸡蛋走到十位数以9开头的区块里需要的次数太多了。

明白了这个缺陷,也就知道了改进的基本思想:还是要对100找出一种二维区块划分,但不是均匀划分。对于比较小的楼层部分,其包含的楼层范围可以适当多;越向大数部分走,其包含的楼层范围越来越小。从下往上,每一个区块内所含楼层递减。

对于最高安全楼层比较低的情况,第一个鸡蛋试的次数少;所以最高安全楼层比较高的情况,则让第二个鸡蛋试的次数少。用第二个鸡蛋的尝试次数的减少来弥补第一个鸡蛋需要尝试的次数的递增,使两个鸡蛋在不同维度上的尝试次数达到一种微妙的平衡。

按照这个思路,要把上面那个均匀的区块切分改进如下:

1,2,3,4……,x   ——区块1
x+1,x+2,……	  ——区块2
……
91,92,93,94	  ——区块n-3
95,96,97,	  ——区块n-2
98,99,	      ——区块n-1
100,    	  ——区块n  

由于每个区块逐次递减1,所以最后的区块里包含1个楼层;相应地,区块总数也可能会变化,不再是10个区块。

那么这里的x和n会是多少呢?

最后一个区块里包含1个楼层,

倒数第二个包含2个楼层,

倒数第三个包含3个楼层,

继续,各区块包含的楼层是4,5,6,……

于是问题转变为,到包含多少个楼层的时候,100个楼层全部分配完?

由于1+2+3+……+13=91,

而1+2+3+……+14=105,

就是说从1开始累加,加到14时,总和第一次大于100:

所以上图里的x是14。

答案揭晓

注意,从14加到1的和是105,还多了5,多的部分可在各区块内分配扣减,不同的扣减方法会对应不同楼层分布的答案。

比如把原来10*10的划分进行如下调整:

10,  10,  10,  10,  10,  10,  10,  10,  10,  10,  
14,  13,  12,  11,  10, 9,  8, 7,  6,  5,  4,  1,

这样对应的答案就是:

先上14楼把第一个鸡蛋扔下去。

没碎,则上(14+13=)27楼再试。

还没碎,上(27+12=)39楼试。

还没碎,上(39+11=)50楼试。

还没碎,上(50+10=)60楼试。

还没碎,上(60+9=)69楼试。

还没碎,上(69+8=)77楼试。

还没碎,上(77+7=)84楼试。

还没碎,上(84+6=)90楼试。

还没碎,上(90+5=)95楼试。

还没碎,上(95+4=)99楼试。

还没碎,上100楼试。

如果中间任何一次鸡蛋碎了,则用第二个鸡蛋从前一次尝试的下一层开始逐层试。如果14楼的第一次尝试就碎了,那么用第二个鸡蛋从1楼开始逐层试。

这时我们来检查一下,当最高安全楼层为几个比较讨厌的特殊值最后的边界值时,分别需要尝试的次数:

——最高安全楼层为13时,第一个鸡蛋第1次就碎了。第二个鸡蛋从1逐层试到13要试13次,一共试了1+13=14次。

——最高安全楼层为26时,第一个鸡蛋试了14,27,在第2次碎了。第二个鸡蛋从15逐层试到26,要试12次,一共是2+12=14次。

——最高安全楼层为38时,第一个鸡蛋试了14,27,39,在第3次碎了。第二个鸡蛋从28试到38,要试11次,一共是3+11=14次。

……………………

——最高安全楼层为98时,第一个鸡蛋试了14,27,39,50,60,69,77,84,90,95,99,在第11次碎了。第二个鸡蛋从96试到98试3次,一共是11+3=14次。

——最高安全楼层为99时,第一个鸡蛋试了14,27,39,50,60,69,77,84,90,95,99,100,在第12次碎了。第二个鸡蛋不用试。一共是12次。

——最高安全楼层为100时,第一个鸡蛋试了14,27,39,50,60,69,77,84,90,95,99,100,最后也没碎,第二个鸡蛋还是不用试。一共也是12次。

最后的两个情况是边界的特殊情况,之前的各次尝试都已经是最糟糕的情况,其他的所有情况总尝试次数都会小于14:

比如最高安全楼层是24,第一个鸡蛋试了14,27,在第2次碎了。第二个鸡蛋从15到25试11次,一共是2+11=13次。

再比如最高安全楼层为71时,第一个鸡蛋试了14,27,39,50,60,69,77,在第7次碎了。第二个鸡蛋从70到72试3次,一共是7+3=10次。

综上,问题解决:第一个鸡蛋依次试14,27,39,50,60,69,77,84,90,95,99,100。中间任何一次破碎了,就从上一次的下一层开始用第二个鸡蛋逐层尝试,直至第二个鸡蛋也破碎为止。

用这个方法,总次数一定不超过14次:当最高安全楼层越来越高时,第一个鸡蛋试的次数越来越多,但第二个鸡蛋试的次数越来越少,两者始终维持着一种平衡。

 

余 音

1、14到底怎么算出来的?

——从x递减到1的数列要累加超过100,所以14其实是满足x(x+1)/2>100的最小正整数。

更进一步说,如果总楼高不是100而是n,则解不等式x(x+1)/2>n得到的最小正整数x就是用第一个鸡蛋首次尝试所迈开的“步长”,也是使用正确方法所得到的最少尝试次数。

2、本题解答唯一吗?

——如前所述,14这个最少尝试次数是满足x(x+1)/2>100的最小正整数,这是唯一的。但14递减到1的数列累加为105,比100多出的部分可在各个区域任意分配,这样对应具体的楼层分布方案会有所不同,但都可行。比如用第一个鸡蛋依次尝试13,25,36,46,55,64,72,79,85,90,94,97,99,100就是另一个正确解答,其最少尝试次数也是14。

3、思考的难点在哪里?

——难点在于想到用两个鸡蛋分别控制两个维度,并且两个维度之间要保持微妙的平衡,剩下的思考过程就比较自然了。但是涉及到与具体的数打交道时还是要细心,尤其是在处理边界情况时要格外小心。

 

LeetCode 887. 鸡蛋掉落

你将获得 K 个鸡蛋,并可以使用一栋从 1 到 N 共有 N 层楼的建筑。

每个蛋的功能都是一样的,如果一个蛋碎了,你就不能再把它掉下去。

你知道存在楼层 F ,满足 0 <= F <= N 任何从高于 F 的楼层落下的鸡蛋都会碎,从 F 楼层或比它低的楼层落下的鸡蛋都不会破。

每次移动,你可以取一个鸡蛋(如果你有完整的鸡蛋)并把它从任一楼层 X 扔下(满足 1 <= X <= N)。

你的目标是确切地知道 F 的值是多少。

无论 F 的初始值如何,你确定 F 的值的最小移动次数是多少?

示例 1:

输入:K = 1, N = 2
输出:2
解释:
鸡蛋从 1 楼掉落。如果它碎了,我们肯定知道 F = 0 。
否则,鸡蛋从 2 楼掉落。如果它碎了,我们肯定知道 F = 1 。
如果它没碎,那么我们肯定知道 F = 2 。
因此,在最坏的情况下我们需要移动 2 次以确定 F 是多少。
示例 2:

输入:K = 2, N = 6
输出:3
示例 3:

输入:K = 3, N = 14
输出:4

提示:

1 <= K <= 100
1 <= N <= 10000

PS:

 * 鸡蛋掉落,谷歌面试题, 
 * 有 K 个鸡蛋,有 N 层楼,用最少的操作次数 F 检查出鸡蛋的质量。
 *
 * 思路:
 * 本题应该逆向思维,若你有 K 个鸡蛋,你最多操作 F 次,求 N 最大值。
 *
 * dp[k][f] = dp[k][f-1] + dp[k-1][f-1] + 1;//这里就当成当前这一层所能决定的层数为鸡蛋没摔的层数+鸡蛋摔了的层数+当前这一层
 * 解释:
 * 0.dp[k][f]:如果你还剩 k 个蛋,且只能操作 f 次了,所能确定的楼层。
 * 1.dp[k][f-1]:蛋没碎,因此该部分决定了所操作楼层的上面所能容纳的楼层最大值
 * 2.dp[k-1][f-1]:蛋碎了,因此该部分决定了所操作楼层的下面所能容纳的楼层最大值
 * 又因为第 f 次操作结果只和第 f-1 次操作结果相关,因此可以只用一维数组。
class Solution {
//二维数组的解法
//    public int superEggDrop(int K, int N) {
//         if (N == 1) {
//             return 1;
//         }
//         int[][] f = new int[N + 1][K + 1];
//         for (int i = 1; i <= K; ++i) {
//             f[1][i] = 1;
//         }
//         int ans = -1;
//         for (int i = 2; i <= N; ++i) {
//             for (int j = 1; j <= K; ++j) {
//                 f[i][j] = 1 + f[i - 1][j - 1] + f[i - 1][j];
//             }
//             if (f[i][K] >= N) {
//                 ans = i;
//                 break;
//             }
//         }
//         return ans;
//     }

//一维数组的解法
    public int superEggDrop(int K, int N) {
        int[] dp = new int[K + 1];
        int ans = 0;    // 操作的次数
        while (dp[K] < N){
            for (int i = K; i > 0; i--) // 从后往前计算
                dp[i] = dp[i] + dp[i-1] + 1;
            ans++;
        }
        return ans;
    }
 
}