887. 鸡蛋掉落 HARD难度

解题思路参考链接:https://github.com/Shellbye/Shellbye.github.io/issues/42

 

你将获得 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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/super-egg-drop
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

 

暴力解法

超时

首先,我们来看扔鸡蛋这个事儿本身,假设在N层的高楼中有K个鸡蛋,这个时候我们在n层扔了一个鸡蛋,那么这一次动作,把整个高楼其实就分成了两部分,一部分是1楼到n楼,这是一个层高为n的新楼,我们暂时叫它一号楼;另一部分是n+1N楼,这是一栋新产生的层高为N-(n+1)+1=N-n的新楼,我们叫他二号楼。
然后,我们来看刚扔下去的鸡蛋,如果它碎了,说明楼层太高(起码高于F),那么F应该是在一号楼,那么我们就带着剩下的K-1个鸡蛋去一号楼继续,当我们站在一号楼的某一层的时候,其实和最开始是一样的(递归的信号)。如果鸡蛋没碎,说明楼层不够高(低于F),此时我们要去二号楼,但是这里有一点点需要注意的,就是我们在二号楼的某一层的时候,其实该层在原始的楼里是要比当前楼层高n层的,其他同理。
最后,因为我们是要找无论 F 的初始值如何的条件下的查找次数,所以我们要在一号楼和二号楼各自的查找次数中选择那个最大的值,用计算机语言描述整个过程,就是

searchTime(K, N) = max( searchTime(K-1, X-1), searchTime(K, N-X) )

其中的X就是我们刚才扔鸡蛋做在的楼层,它具体是哪一层并不重要。对于从1到N的每一个X,都可以计算出一个对应的searchTime的值,在这N个值中,最小的那个就是本题的答案了!

class Solution(object):
    def superEggDrop(self, K, N):
        """
        :type K: int
        :type N: int
        :rtype: int
        """
#         法一:递归
        return self.help(K,N)
    
    def help(self,K,N):
#         递归结束条件
        if K==1 or N==1 or N==0:return N
        min_res=N
        for i in range(1,N+1):
#             加1表示移动一次,把当前楼层分为上下两部分,根据鸡蛋是否破损,分别递归
            temp=max(self.help(K-1,i-1),self.help(K,N-i))+1
            min_res=min(min_res,temp)
        return min_res
        

上面的这个方法有多暴力呢?通过推导式我们发现它和斐波那契数列第N项的计算方法(Fib(n) = Fib(n-1) + Fib(n-2))类似,那么,这个东西的时间复杂度又怎么计算呢?对于第N项 Fib(n) = Fib(n-1) + Fib(n-2),这个计算本身就是一个加法,其时间复杂度是O(1),但是其中包含了第N-1和第N-2两项,他们又同理本身需要一个O(1),且也分别包含了第N-1-1、第N-1-2和第N-2-1、第N-2-2四项,以此类推,这就是一个满二叉树的节点总数求和(如图),即O(2^n),同样的,其空间复杂度很低,仅仅是O(1)

image

动态规划(实质将递归的中间结果保存下来)

上面的计算之所以时间复杂度高,与递归版本的斐波那契数列一样,就是因为重复计算了很多遍底部节点的值,为了加快这个计算过程,一个简单的提升方法就是拿空间换时间,把计算的中间结果都存储起来,后面直接查表即可。

class Solution(object):
    def superEggDrop(self, K, N):
        """
        :type K: int
        :type N: int
        :rtype: int
        """
#         法二:动态规划 dp[k][n]=max(dp[k-1][i-1]+dp[k][n-i])+1
        dp=[[0]*(N+1) for i in range(K+1)]
#         dp初始化
        for i in range(1,N+1):
#         0个鸡蛋
            dp[0][i]=0
#         1个鸡蛋
            dp[1][i]=i
        for i in range(1,K+1):
#             0层
            dp[i][0]=0
#         从第2个鸡蛋开始dp
        for i in range(2,K+1):
            for j in range(1,N+1):
                min_res=N
                for x in range(1,j+1):
                    min_res=min(min_res,max(dp[i-1][x-1],dp[i][j-x])+1)
                dp[i][j]=min_res
        return dp[K][N]

这个解法利用了一个二维数组存储了部分计算结果(空间复杂度O(KN)),使得时间复杂度降低到了O(KN^2)。但是依然是一个平方级别的时间复杂度,不够快,还能优化吗?

 

基于二分查找的动态规划法

最开始的时候,我们就想到了二分查找,但是因为发现不对,就果断抛弃了,事实上它还是有利用价值的。在上一个O(KN^2)的算法中,我们拿着K个鸡蛋检查了每一个楼层来寻找F,但是事实上这并不是必须的,为什么呢?我们来看我们上面总结的这个递归的等式

searchTime(K, N) = max( searchTime(K-1, X-1), searchTime(K, N-X) )

现在我们令T1 = searchTime(K-1, X-1)T2 = searchTime(K, N-X),其中,T1是随着X的增长而增长的,T2是随着X的增长而降低的,如下图


其中蓝色描出来的部分,就是searchTime(K, N)了,我们可以看出来它是局部有序的,所以可以考虑二分查找之!

交点就是我们要求解的点,显然可用二分查找(比较T1与T2的大小来移动X)!

这里有一些与简单二分查找不一样的地方,就是在简单二分查找中,我们拿到输入数组A和它的下标lowhighmid之后,比较大小直接就是用下标读取数组的值A[low] < A[mid],但是在我们这个二分查找中,这个值是需要依赖与本题逻辑相关的一些计算的,具体看代码

class Solution(object):
    def superEggDrop(self, K, N):
        """
        :type K: int
        :type N: int
        :rtype: int
        """
#         法三:二分搜索+动态规划 dp[k][n]=max(dp[k-1][i-1]+dp[k][n-i])+1
#     定义一个成员变量充当全局变量,保存递归中间值    
        self.memo={}
        return self.help(K,N)
    
    def help(self,K,N):
        if N==0:return 0
        if K==1:return N
        key=N*1000+K
        if key in self.memo.keys():
            return self.memo[key]
        left,right=1,N
#         二分
        while left<right-1:
            middle=(left+right)//2
            low_val=self.help(K-1,middle-1)
            heigh_val=self.help(K,N-middle)
            if low_val<heigh_val:
                left=middle
            elif low_val>heigh_val:
                right=middle
            else:left=right=middle
#         比较left和right对应处的值
        temp=min(max(self.help(K-1,left-1),self.help(K,N-left)),max(self.help(K-1,right-1),self.help(K,N-right)))+1
        self.memo[key]=temp
        return temp

更快的方法

这个方法是上一个方法的延续,并把时间复杂度从O(KNlogN)降到了O(KN),下面我们看一下它的思路。
首先令Xa = opt(K,N)是能够找最小移动次数的最小的X,根据上一个方法中我们分析T1T2的单调性的方法,我们可以再次分析,并可以最终得出opt(K,N)是随着N的增长而增长的,这个我们也可以从下图中看到

 

随着N的增长,T2在向上移动,他们的交叉点Xa也在向上移动,那么在上一个方法中需要遍历从1X的循环就可以改成从XaX了,因为小于Xa的都找不到最小移动次数。
与上一个方法自顶向下的方法不同,这次的解法是自底向上的,它每次的计算都是会先找到Xa,然后就可以直接计算出结果。

 

class Solution(object):
    def superEggDrop(self, K, N):
        """
        :type K: int
        :type N: int
        :rtype: int
        """
#         法四:最优化策略动态规划
        dp=[0]*(N+1)
#     初始化k=1情况
        for i in range(N+1):
            dp[i]=i
            
        for k in range(2,K+1):
            dp2=[0]*(N+1)
            x=1
            for n in range(1,N+1):
#                 寻找最优的x
# max(dp[x-1],dp2[n-x])>max(dp[x],dp[n-x-1])表示上一次较低层比当前层的最优值大
                while x<n and max(dp[x-1],dp2[n-x])>max(dp[x],dp2[n-x-1]):
                    x+=1
                dp2[n]=1+max(dp[x-1],dp2[n-x])
            dp=dp2
        return dp[N]

这里需要特别注意一下dp,在for循环中,它代表是上一次循环解出来的最小值,也就是比当前楼层低一层的情况下的最优解。所以while条件中的
max(dp[x - 1], dp2[n - x]) > max(dp[x], dp2[n - x - 1])
带入T1 =dp(K−1,X−1)T2 =dp(K,N−X),就会发现其实是
max(T1(x-1), T2(x-1)) > max(T1(x), T2(x))

换个思路

上面的方法的思路,都还是顺着题目的思路的进行的,其实我们可以换一个思路来想:“求k个鸡蛋在m步内可以测出多少层”。我们令dp[k][m]表示k个鸡蛋在m步内可以测出的最多的层数,那么当我们在第X层扔鸡蛋的时候,就有两种情况:

  1. 鸡蛋碎了,我们少了一颗鸡蛋,也用掉了一步,此时测出N - X + dp[k-1][m-1]层,X和它上面的N-X层已经通过这次扔鸡蛋确定大于F
  2. 鸡蛋没碎,鸡蛋的数量没有变,但是用掉了一步,剩余X + dp[k][m-1]X层及其以下已经通过这次扔鸡蛋确定不会大于F

也就是说,我们每一次扔鸡蛋,不仅仅确定了下一次扔鸡蛋的楼层的方向,也确定了另一半楼层与F的大小关系,所以在下面的关键代码中,使用的不再是max,而是加法(这里是重点)。评论里有人问到为什么是相加,其实这里有一个惯性思维的误区,上面的诸多解法中,往往求max的思路是“两种方式中较大的那一个结果”,其实这里的相加,不是鸡蛋碎了和没碎两种情况的相加,而是“本次扔之后可能测出来的层数 + 本次扔之前已经测出来的层数”。

dp[k][m]表示k个鸡蛋在m步内可以测出来的的最大层数

dp[k][m]=dp[k-1][m-1]+dp[k][m-1]+1

class Solution(object):
    def superEggDrop(self, K, N):
        """
        :type K: int
        :type N: int
        :rtype: int
        """
#         法五:换个思路 二维动态规划 dp[k][m]表示k个鸡蛋在m步内可以测出来的的最大层数 dp[k][m]=dp[k-1][m-1]+dp[k][m-1]+1
        dp=[[0]*(N+1) for i in range(K+1)]
#     主要:必须先遍历m,因为要寻找当前m步下,花费一定数量鸡蛋能测到的最大层数
        for m in range(1,N+1):
            for k in range(1,K+1):
                dp[k][m]=dp[k-1][m-1]+dp[k][m-1]+1
                if dp[k][m]>=N:
                    return m
#         最多需要N步
        return N
                
            
        

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值