题目描述
你将获得 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
解法一:动态规划
1. 递归
1.1暴力
- 状态:当前拥有的鸡蛋数K和需要测试的楼层数N,随着测试变化
- 选择:选择那层楼仍鸡蛋
框架:
// 当前状态为 K 个鸡蛋,面对 N 层楼
// 返回这个状态下的最优结果
public int dp(int K, int N){
int res;
for(int i=0;i<n;i++)
res = Math.min(res, 这次在第 i 层楼扔鸡蛋);
return res;
}
- 状态转移:鸡蛋碎了,dp(K-1,i-1),鸡蛋没碎:dp(K,N-i) 其中i层楼相当于第0层
// 当前状态为 K 个鸡蛋,面对 N 层楼
// 返回这个状态下的最优结果
public int dp(int K, int N){
int res;
for(int i=0;i<n;i++)
res = Math.min(res,
Math.max(dp(K-1,i-1)//碎了
,dp(K,N-i) //没碎
)+1 //第i楼扔了1次
);
return res;
}
Base case
public int dp(int K,int N){
if(K==1) return N;
if(N==0) return 0;
}
最终代码
//6,5000超时
class Solution {
int[][] memo;
public int superEggDrop(int K, int N) {
memo=new int[K+1][N+1];
return dp(K,N);
}
public int dp(int K,int N){
int res=Integer.MAX_VALUE;
if(K==1) return N;
if(N==0) return 0;
if(memo[K][N]!=0){
return memo[K][N];
}
for(int i=1;i<N+1;i++){
res=Math.min(res,Math.max(dp(K,N-i),dp(K-1,i-1))+1);
}
memo[K][N]=res;
return res;
}
}
算法的总时间复杂度是 O(K*N^2), 空间复杂度 O(KN)。
1.2二分查找优化
两个关于i的函数,前者随i的增加而单调递增,后者随i的增加单调递减
最低点代码:
for (int i = 1; i <= N; i++) {
if (dp(K - 1, i - 1) == dp(K, N - i))
return dp(K, N - i);
}
代码如下:
class Solution {
public int superEggDrop(int K, int N) {
int[][] memo=new int[K+1][N+1];
return dp(K,N,memo);
}
public int dp(int k,int n,int[][] memo){
if(n==0) return 0;
if(k==1) return n;
int res=Integer.MAX_VALUE;
if(memo[k][n]!=0){
return memo[k][n];
}
int lo=1,hi=n;
while(lo<=hi){
int mid=(lo+hi)/2;
int broken=dp(k-1,mid-1,memo);
int not_broken=dp(k,n-mid,memo);
if(broken>not_broken){
hi=mid-1;
res=Math.min(res,broken+1);
}
else{
lo=mid+1;
res=Math.min(res,not_broken+1);
}
}
memo[k][n]=res;
return res;
}
}
算法的总时间复杂度是 O(KNlogN), 空间复杂度 O(KN)。
2.建表
class Solution {
public int superEggDrop(int K, int N) {
int[][] dp = new int[K+1][N+1];
//初始化
for(int i=1;i<=K;i++){
dp[i][1]=1;
}
for(int j=1;j<=N;j++){
dp[1][j]=j;
}
//逐行逐列二分
for(int i=2;i<=K;i++){
for(int j=2;j<=N;j++){
int lo=1,hi=j;
while(lo<=hi){
int mid=lo+(hi-lo)/2;
int broken=dp[i-1][mid-1];
int not_borken=dp[i][j-mid];
if(broken == not_borken){
lo = mid;
break;
}
if(broken>not_borken){
hi=mid-1;
}
else{
lo=mid+1;
}
}
//max而不是min
dp[i][j]=Math.max(dp[i-1][lo-1],dp[i][j-lo])+1;
}
}
return dp[K][N];
}
}
3.重新定义状态方程
3.1二维dp
确定当前鸡蛋个数和最多允许的扔鸡蛋次数,就能确定F的最高楼层。
dp[k][m]=n;
//当前由k个鸡蛋,可以尝试仍m次鸡蛋
//这个状态下,最坏情况最多能确切测试n层的楼
//比如dp[1][7]=7表示:
//现在有1个鸡蛋,允许你仍7次
//这个状态最多给你测7层楼
最终求的是扔鸡蛋次数m
int superEggDrop(int K, int N) {
int m = 0;
while (dp[K][m] < N) {
m++;
// 状态转移...
}
return m;
}
1、无论你在哪层楼扔鸡蛋,鸡蛋只可能摔碎或者没摔碎,碎了的话就测楼下,没碎的话就测楼上。
2、无论你上楼还是下楼,总的楼层数 = 楼上的楼层数 + 楼下的楼层数 + 1(当前这层楼)。
转移方程:
dp[k][m]=dp[k][m-1]+dp[k-1][m-1]+1
dp[k][m - 1]
就是楼上的楼层数,因为鸡蛋个数 k 不变,也就是鸡蛋没碎,扔鸡蛋次数 m 减一;
dp[k - 1][m - 1]
就是楼下的楼层数,因为鸡蛋个数 k 减一,也就是鸡蛋碎了,同时扔鸡蛋次数 m 减一。
class Solution {
public int superEggDrop(int K, int N) {
int[][] dp=new int[K+1][N+1];
//base case:
//dp[0][..]=0;
//dp[..][0]=0;
int m=0;
while(dp[K][m]<N){
m++;
for(int k=1;k<=K;k++){
dp[k][m]=dp[k][m-1]+dp[k-1][m-1]+1;
}
}
return m;
}
}
等同于
for (int m = 1; dp[K][m] < N; m++)
for (int k = 1; k <= K; k++)
dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1;
3.2再优化为一维数组
因为第 f 次操作结果只和第 f-1 次操作结果相关,因此可以只用一维数组。
dp[i]=dp[i]+dp[i-1]+1
class Solution {
public int superEggDrop(int K, int N) {
int[] dp=new int[K+1];
int ans=0;
while(dp[K]<N){
//dp[i-1]是上一次操作的,故要用逆序,不然会被覆盖
for(int i=K;i>0;i--)
dp[i]=dp[i]+dp[i-1]+1;
ans++;
}
return ans;
}
}