2018 年力扣高频算法面试题汇总-难题记录-鸡蛋掉落

题目描述:

你将获得 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. 1 <= K <= 100
  2. 1 <= N <= 10000

 

要完成的函数:

int superEggDrop(int K, int N) 

 

说明:

1、这道题给定K个鸡蛋和N层楼,想要测试鸡蛋壳的保护能力,最高从第几层楼摔下来鸡蛋不会碎,而从高一层楼摔下来鸡蛋就会碎。最高不会碎的这一层称为第F层。

现在F层具体是哪一层不知道,而你有K个鸡蛋,一共有N层楼,问需要尝试多少次扔鸡蛋才能知道F是多少。

如果鸡蛋摔碎了就不能再用,如果鸡蛋没碎那么还可以继续扔鸡蛋下楼做测试。

鸡蛋个数K在1到100的闭区间,楼层个数N在1到10000的闭区间,F是0到N的闭区间。

 

2、(下面的思路分享很长,但是思路很详细,基本记录了笔者从粗浅到最终解决方案的整个思考过程)

题意有点绕,举个例子说明一下。

假设你只有1个鸡蛋,2层楼,那么你只能从一楼开始扔,扔下去如果摔碎了,那么说明F是0,因为如果F是1的话,从一楼摔下去鸡蛋不会碎的。此时只需要1次扔鸡蛋。

如果从一楼摔下去不会碎,那么F是1或者2,那么就尝试从二楼扔下来,如果碎了,那么F是1;如果没碎,那么F是2。此时需要扔2次鸡蛋。

所以在不知道F具体是多少的情况下,最小需要扔2次鸡蛋才能确定F是多少。

 

有的同学可能会问,为什么只能从一楼开始?不能从二楼开始吗?

二楼扔下去如果没碎,那么还好,F肯定是2。

但是如果碎了呢?F有可能是1,也有可能是0,而你唯一的一个鸡蛋碎了,测试失败。

所以在只有1个鸡蛋的情况下,只能老老实实从一楼开始测试,逐渐增加楼层高度。

 

如果有2个鸡蛋,100层楼呢,最少的扔鸡蛋次数是多少?

笔者最开始的想法是,可以先拿一个鸡蛋做测试,尽可能减少测试空间。比如在50层抛下来,如果碎了那么测试空间就是[1,49];如果没碎,那么测试空间是[51,100]。

这里解释一下为什么碎了,测试空间就是[1,49];没碎,测试空间就是[51,100]。

如果鸡蛋碎了,那么只剩下一个鸡蛋,F的取值范围是[0,49],我们唯一的鸡蛋只能从一楼开始扔。按照我们的经验,1个鸡蛋49层楼,那么一共需要扔49次才能确定F的具体值。

也就是说当F的取值范围是[0,49]的时候,扔鸡蛋的测试空间是[1,49]。

那么如果鸡蛋没碎,F的取值范围是[50,100],那么扔鸡蛋的测试空间就是[51,100],从51楼开始扔,一直到100楼扔,必定可以确定F的具体值。

 

笔者这种想法比较直接,但是扔鸡蛋的次数不是最少的。

比如F是49,在50层扔下去,鸡蛋碎了,只剩下一个鸡蛋,要从1楼开始扔,扔49次到达49层,最终确定F的值。

一共需要扔1+49次。

第二个鸡蛋扔49次也太憋屈了吧!第一个鸡蛋只发挥了一次价值就碎了。

 

那我们让两个鸡蛋发挥价值能发挥得均衡一点。

100层楼,开方,10,第一个鸡蛋尝试10次来确定测试区间,第二个鸡蛋也只需要在确定的小区间中尝试。

这种方法的最坏情况就是F为99,第一个鸡蛋10层扔一下,20层扔一下,……100层扔一下,一共需要10次。确定了F的取值范围是[90,99]。

那么第二个鸡蛋从91开始尝试,一直扔到99,一共需要9次。

所以采取这种均衡的方案,最坏情况只需要10+9=19次,比起二分的方法好多了。

 

但是这种均衡的方案也不是最佳的,同学们可以参考一下这篇文章:https://juejin.im/post/5b98785de51d450e71250aab

 

具体到这道题,我们不再是只有2个鸡蛋和100层楼了,我们现在有K个鸡蛋和N层楼,要求最少的扔鸡蛋次数。

其实换个思路,每次扔鸡蛋下楼,不就是碎和没碎两种结果吗。

假设我们在X层扔,如果碎了,那么还有K-1个鸡蛋,X-1层楼待验证;如果没碎,那么还有K个鸡蛋,N-X层楼待验证。这个时候我们已经扔了一次鸡蛋了。

所以其实我们可以把问题转化为两个子问题,K个鸡蛋N层楼最少的扔鸡蛋次数=K-1个鸡蛋X-1层楼最少的扔鸡蛋次数+1,或者是=K个鸡蛋N-X层楼最少的扔鸡蛋次数+1。

这两个子问题肯定有一个的扔鸡蛋次数比较大,我们要取那个大的,所以列出式子就是

record[K][N] = max ( record[K-1][X-1] , record[K][N-X] ) + 1

X不确定要取多少,经过二分法和均衡法的比较,我们发挥二分的方法也不是最佳的……那要不X就从1到N都试一下吧!如果在某一层扔,可以求得record[K][N] = max ( record[K-1][X-1] , record[K][N-X] ) + 1是最小的,那么我们就能确定X在这一层扔是最佳方案。

所以式子最终可以写成record[K][N] = min ( max ( record[K-1][X-1] , record[K][N-X] ) + 1 ) , 1<=X<=N

 

也就是说,这道题可以用分治法的思想来做,把一个问题分成两个子问题,最终把两个子问题的解汇总,得到原问题的解。

同时,由于record记录的这些数值要多次使用,所以为了减少时间复杂度,我们就不用分治法,改用动态规划的查表法来做。

完美符合凌应标老师在课上多次强调的动态规划三个特点哈哈哈:

1、最优子结构性质——原问题的最优解可以由多个子问题的最优解得到

2、重复子问题(分治法与动态规划的最大区别)

3、子问题的个数有限

 

动态规划的代码如下:

    int superEggDrop(int K, int N) 
    {
        vector<vector<int>>record(K+1,vector<int>(N+1,0));//需要从0个鸡蛋或者0层楼开始算起,所以申请了K+1行N+1列的空间 
        for(int i=1;i<=K;i++) 
            record[K][1]=1;//如果只有一层楼,那么无论多少个鸡蛋都只需要扔一次鸡蛋 
        for(int i=1;i<=N;i++)
            record[1][i]=i;//如果只有一个鸡蛋,那么有多少层楼就需要扔多少次鸡蛋 
        for(int i=2;i<=K;i++)//从两个鸡蛋两层楼的情况开始算起 
        {
            for(int j=2;j<=N;j++)
            {
                int t=INT_MAX;
                for(int l=1;l<=j;l++)
                    t=min(t,max(record[i-1][l-1],record[i][j-l])+1);
                record[i][j]=t;//记录最小的扔鸡蛋次数到record[i][j]中 
            }
        }
        return record[K][N];//最后返回record[K][N]就是我们要找的扔鸡蛋次数了 
    }

上述代码没有通过所有的样例测试……因为超时了……

可以看到三重循环,肯定耗时很多,比如当K=5,N=10000。

 

3、如何优化?hhh

同样还是受上面分享的掘金那篇文章的启发,我们换个角度来想这个问题。

如果给你K个鸡蛋和M次尝试摔鸡蛋的次数,那么你最多可以测算出多高的楼层,无论F具体是在哪一层。

假设可以测算的最高楼层是N,那么题意也就是说给K个鸡蛋和M次次数,必定可以测算得到N层楼的F的具体值。

那还是分治法的思路来看,在某一层X摔下去,如果碎了,那么只剩下M-1次次数和K-1个鸡蛋,而用这剩下的M-1次次数和K-1个鸡蛋,必定可以测算得到X-1层的F的具体值。

如果没碎,那么还剩下M-1次次数和K个鸡蛋,而用这M-1次次数和K个鸡蛋,必定可以测算得到N-X的楼层的F的具体值。

我们用record[M][K]代表,用M次次数和K个鸡蛋,最高能测算的楼层高度。

record[M-1][K-1]代表,用M-1次次数和K-1个鸡蛋,最高能测算的楼层高度。

record[M-1][K]代表,用M-1次次数和K个鸡蛋,最高能测算的楼层高度。

可以有record[M][K] = record[M-1][K-1] + record[M-1][K] + 1

+1是因为加上当前层的楼层高度。

这个式子理解得不是很清晰的话,同学们自己再琢磨一下,再回看3中的话。这个式子想要清晰理解需要费些思索。

可以结合record[2][2]的情况来理解,再试下record[3][2]的情况。

 

有了这个式子,我们能求M次次数和K个鸡蛋的情况下,最高能测多少层。

但题目求的是层数确定了,鸡蛋个数确定了,要求M的具体值。

其实一样的,比如确定鸡蛋个数K是3,楼层高度N是14。

假如只有一次尝试次数,3个鸡蛋,那么最高也就能测一层。达不到14的高度。

如果两次尝试次数,3个鸡蛋,那么record[2][3] = record[1][2] + record[1][3] + 1。

record[1][2]和record[1][3]代表一次尝试次数,那么最高只能测一层,所以上式结果是3。同样达不到14的高度。

如果三次尝试次数,3个鸡蛋,那么record[3][3] = record[2][2] + record[2][3] + 1=3+3+1=7。同样达不到14的高度。

如果四次尝试次数,3个鸡蛋,我们会发现record[4][3] = record[3][2] + record[3][3] + 1=6+7+1=14。刚好达到14的高度。

所以其实我们只需要不断尝试下去,最终尝试第M次的时候,发现record[M][K]>=N,那么就可以了。

 

具体写代码的时候,发现我们没办法提前确定M的次数,所以没办法定义一个M行K列的vector来存储数据。

但我们发现其实每次增大尝试次数的时候,都是基于上一次尝试的结果来求解。

所以我们可以只定义一个1行K列的vector,然后不断地更新这一行vector的数值,直到在某次更新之后vector[K]>=N。

 

具体之后的代码如下:

    int superEggDrop(int K, int N) 
    {
        vector<int>record(K+1,1);//包含0个鸡蛋的情况,所以需要申请K+1个空间。只尝试一次的时候,无论多少个鸡蛋,最高都只能测1层
        record[0]=0;//0个鸡蛋的情况特殊化处理,为0
        int move=2;//如果尝试2次
        while(record[K]<N)//当record[K]大于等于N的时候就退出循环
        {
            for(int i=K;i>=1;i--)//从vector的后面开始更新,这样不影响其他位置的vector元素的更新
                record[i]=record[i]+record[i-1]+1;
            move++;//move+1,再尝试一次
        }
        return move-1;//返回需要的尝试次数
    }

可以说是非常简洁了。题目十分复杂,但是想明白之后,写一个二重循环就可以解决这道题目。

花了两个小时写出这篇思路比较详细的文章,作为一个自我记录,也希望可以给后来者些许启发。

因为时间紧,排版和语言组织什么的可能不是很好,有哪里写得不好欢迎同学们在评论区提出~

posted @ 2019-03-13 16:09 chenjx85 阅读(...) 评论(...) 编辑 收藏