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)