leetcode 887. Super Egg Drop(动态规划,二分查找,剪枝)

题目链接

leetcode 887. Super Egg Drop

题目大意

有N层楼,K个鸡蛋,判断移动多少次鸡蛋可以知道鸡蛋最开始摔碎的楼层。需要注意的是,鸡蛋如果在k层没被摔碎,下一次还可以直接从1层拿到k+1层进行实验;反之如果在k层摔碎了,这个鸡蛋就没用了

样例分析

样例 1:
Input: K = 1, N = 8
Output: 8
分析:
如果只有一个鸡蛋的话,那么只能从第一层楼开始扔,如果第一层楼碎了,那么说明最低摔碎的楼层就是第一层;反之,如果第一层没有摔碎,我们把鸡蛋从第二层往下摔,以此类推…一直到第8层,如果第八层鸡蛋碎了,说明最低就是八层;那如果到了第八层还没有碎,那可以知道最低摔碎楼层就是第九层了。由于判断出确切的最低楼层,最坏情况下需要8次,所以答案是8

样例 2:
Input: K = 5, N = 1
Output: 1
如果只有一层楼,那不管前面的鸡蛋有多少个,都只需要扔一次就清楚了,结果始终都是1

样例 3:
Input: K = 2, N = 6
Output: 3
当N和K都不为1时,这时就要考虑最普遍的情况了。先说一下样例3结果的来源,第1个鸡蛋可以从3楼开始扔

  • 如果3楼碎了,说明肯定楼层一定在1 ~ 3楼之间。这时候我们从1楼开始扔,如果碎了,说明最低摔碎楼层在1楼,一共扔了2次;如果没碎,再去2楼扔,根据2楼的结果一定知道最低楼层,这种情况需要扔3次
  • 如果3楼没碎,说明最低摔碎楼层在4 ~ 6楼(注意6楼可能也不能摔碎),这时候我们去5楼扔,如果没碎,说明最低楼层在4楼,一共扔了2次;如果碎了,说明最低楼层在5 ~ 6楼,这时候去6楼扔,一定可以得到答案,一共需要扔3次
  • 综合上面的结果,最坏情况下扔3次一定能确定出最低的楼层
解题思路
分析1:记忆化搜索O(KN^2)

上面分析了几个比较简单的情况,但是当N和K都比较大时,很难直观的知道每次该从那个楼层扔。在这里首先推荐李永乐老师的B站分析视频:双蛋问题bilibili

相信看完了之后对这个问题会有一个更加直观的认识,此处简单对视频中的算法做一个总结:
设n, k分别为楼层数,鸡蛋数,我们需要求解的结果为dp[n][k],表示n层楼,k个鸡蛋确定最低楼层至少需要移动的步数。由于我们并不知道到底从那一层楼往下扔可以得到最优解,不妨我们假设从t(1=<t<=N)层可以得到最优解,根据在t层碎与不碎分为两种情况:

  • 如果t层摔碎了,说明最低楼层在1 ~ t层,这时候我们还剩k-1个鸡蛋(摔碎的鸡蛋不能再用),那么dp[n][k] <=== dp[t-1][k-1]+1(+1表示在t层摔鸡蛋这一步操作,t-1是因为第t层已经扔过了,我们只需要从前面1 ~ t-1层扔)
  • 如果t层没碎,说明最低楼层在t+1~n层,这时候我们还剩k个鸡蛋,那么dp[n][k]<=== dp[n-t][k]+1(n-t表示后面n-t层)
  • 由于要考虑到最坏情况都能判断出最低楼层,那么有: dp[n][k]=max{dp[t-1][k],dp[n-t][k]}+1

但是,注意到,对于t(1=<t<=N),我们枚举每一个t都会有一个dp[n][k]出现,显然为了让整体的次数最少,我们应该选择最小的dp[n][k]。所以这部分的伪代码应该是:

int ans=INT_MAX;
for(int t=1;t<n;t++){
     int tmp=max(solve(t-1,k-1),solve(n-t,k))+1;
     ans=min(ans,tmp);
 }
dp[n][k]=ans

结合上面思路,不难写出下面代码:
尽管加上记忆化搜索,却还是超时了,分析整体算法的复杂度,为O(KN^2),可知,当N取一个很大的值的时候,一定会超时。

class Solution {
public:
    vector<vector<int>> dp;
    int superEggDrop(int K, int N){
        dp=vector<vector<int>>(N+1,vector<int>(K+1,0));
        
        return solve(N,K);
    }
    
    int solve(int n,int k){
        if(n<=0 || k<=0) return 0;
        if(k==1) return dp[n][k]=n;
        if(n==1) return dp[n][k]=1;
        if(dp[n][k]) return dp[n][k];//记忆化搜索

        int ans=INT_MAX;
        for(int t=1;t<n;t++){
            int tmp=max(solve(t-1,k-1),solve(n-t,k))+1;
            ans=min(ans,tmp);
        }
        dp[n][k]=ans;
        
        return dp[n][k];
    }
    
};
分析2:二分搜索剪枝O(KNlogN)

在这里,我们分析一下上面代码还有哪些可以改进的空间。不难发现,当我们固定好鸡蛋数目k时,楼层数n越高,所花的次数一定不会小于低楼层的次数。对应上面的代码:

int tmp=max(solve(t-1,k-1),solve(n-t,k))+1;

我们发现:如果t取得特别靠近1楼,那么solve(t-1,k-1)的值比较小,但是solve(n-t,k)的值会反而比较大;如果t特别靠近n楼,那么这两项的大小刚好反过来。由于这是一个max()函数,显然当两者都往中间靠近时是最小的,即让solve(t-1,k-1)的值尽可能与solve(n-t,k)的值接近一定是最优解(因为我们需要保证tmp的值尽可能小)。

这就给我们的优化带来了思路。我们可以采用二分查找的方法,初始化left=1, right=n,每次先比较中间值mid=(left+right)/2需要的次数,比如solve(mid-1,k-1)solve(n-mid,k)的值,如果第一项的值比较大,说明最佳楼层t一定在mid左边;反之一定在mid右边。

基于上述分析,不难写出下列代码:
这时候由于查找最优的t采用二分查找,只要O(logN)的复杂度,所以整体算法的复杂度为O(KNlogN)

class Solution {
public:
    vector<vector<int>> dp;
    int superEggDrop(int K, int N){
        dp=vector<vector<int>>(N+1,vector<int>(K+1,0));
        
        return solve(N,K);
    }
    
    //递归超时
    int solve(int n,int k){

        if(n<=0 || k<=0) return 0;
        if(k==1) return dp[n][k]=n;
        if(n==1) return dp[n][k]=1;
        if(dp[n][k]) return dp[n][k];//记忆化搜索
        
        //二分
        //假设(前提):当鸡蛋数K固定时,如果楼层N越高,所需要的次数也越多(或者单调不减)
        int l=1, r=n;
        int minv=INT_MAX;
        
        while(l<r){//优化之后时间复杂度 O(KNlogN)
            int mid=l+(r-l)/2;//当前所在的层数
            int tmpl=solve(mid-1,k-1);//mid-1层,因为mid层已经统计过
            int tmpr=solve(n-mid,k);
            
            minv=min(minv,max(tmpl,tmpr))+1;
            
            if(tmpl==tmpr) break;
            
            else if(tmpl<tmpr){
                l=mid+1;
            }
            else r=mid;
        }
        
        dp[n][k]=minv;
        return dp[n][k];
    }
    
};
参考资料
  • https://www.bilibili.com/video/BV1KE41137PK
  • https://leetcode.com/problems/super-egg-drop/discuss/159055/Java-DP-solution-from-O(KN2)-to-O(KNlogN)
posted @ 2020-11-06 18:55  xzhws  阅读(121)  评论(0编辑  收藏  举报