你将获得 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
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/super-egg-drop
代码思路:
有一个当前序列(k,n).
如果知道有一个x,是当前的最佳分割位置,同时知道分割后的两个序列(k-1,x-1)和(k,n-x)各自需要的移动次数,那么当前序列的返回值就是1+max((k-1,x-1),(k,n-x))。
进而问题转化为最小化这个max((k-1,x-1),(k,n-x))。分析一下:
-
max((k-1,x-1),(k,n-x))和什么参数有关
和x有关,而x又取决于k和n的值,这句话不是废话,当鸡蛋的数量不足以每次二分的时候,就必须减少x的值,尽量往下划分。例如:(2,100)=14第一次的移动x=14,就不是x=50。
-
如何快速有效的找到需要的x。
这里就采用二分法进行搜索,理由,因为当x左右两边的移动次数相近的时候,一定是最小的时候,(k,n)如果看成一个函数,在k一定的时候函数值随着n的增大而增大,减小而减小,(k-1,x-1),(k,n-x),当x-1减小,n-x必然增大,所以在交点附近的x就是我们要找的x。
在二分法的过程中,设区间为front-last,x=(front+last)/2,如果(k-1,x-1)< ( k,n-x),就要让front=x,如果(k-1,x-1)> ( k,n-x)就要让last=x,如果(k-1,x-1)= ( k,n-x),就让front=last=x,就是找到了需要的x。终止条件,这里我学习的Leetcode官方代码,官方还是很强的,front+1<last,这里的判断则是因为如果n是一个偶数,那么一定找不到最中间的值,也或者(k-1,x-1), ( k,n-x)两个一定不相等,那么搜索到区间紧挨着的时候,也就没办法继续二分,进而退出循环。
while(front+1<last) {
x=(front+last)/2;
result1=dfs(k-1,x-1);
result2=dfs(k,n-x);
if(result1>result2) {
last=x;
}else if(result1<result2){
front=x;
}else {
front=x;
last=x;
}
}
剩下的就很简单了,对于区间长度为1的时候还需要判断究竟是要以front为x还是以last为x。所以还需要判断,Math.min(Math.max(dfs(k-1, front-1), dfs(k, n-front)), Math.max(dfs(k-1, last-1), dfs(k, n-last)))
代码如下:
public int superEggDrop(int K, int N) {
if(K==1)return N;
if(N==1)return 1;
if(N==2)return 2;
if(N==3)return 2;
dfs(K,N);
return map.get(N*100+K);
}
public static int dfs(int k,int n) {
if(k==1)return n;
if(n==1)return 1;
if(n==2)return 2;
if(n==3)return 2;
int result1=0;
int result2=0;
int result4=0;
if(map.containsKey(n*100+k)) {
return map.get(n*100+k);
}else {
int front=0;
int last=n;
int x=0;
while(front+1<last) {
x=(front+last)/2;
result1=dfs(k-1,x-1);
result2=dfs(k,n-x);
if(result1>result2) {
last=x;
}else if(result1<result2){
front=x;
}else {
front=x;
last=x;
}
}
result4 = 1 + Math.min(Math.max(dfs(k-1, front-1), dfs(k, n-front)),
Math.max(dfs(k-1, last-1), dfs(k, n-last)));
System.out.println(k+" "+n);
map.put(n*100+k, result4);
}
return map.get(n*100+k);
}
结果如下:
代码分析:
leetcode官方代码的分析很容易懂,所有需要的(k,n)的组合一共有kn个,所以需要记录kn个组合对应的值,每一个组合都进行二分法进行判断,假设所有的节点的值都知道(最简单的组合例如(1,n),(k,1)的值是知道的,进而分支上方的各个组合的值也可以求得,同时每个组合只会进行一次)所以每个组合需要进行(logn)次计算,所以时间复杂度是O(knlogn),空间复杂度O(k*n)。
踩坑分析:
- 遇到题需要合理分析
分割点x不一定是n/2,当k较小而n比较大的时候,x会向下取值,同时向下取值的幅度又和n有关,所以如下代码是错误的。判断过于草率。
public static int dfs1(int k,int n1,int n2,int x,int y) {
if(k==1)return n2-n1;
if(n2-n1==1&&(n2==y&&n1==x)) {
return 1;
}else if(n2-n1==1) {
return 2;
}
if(n2==n1&&(n1==x||n2==y)) {
return 1;
}else if(n2==n1) {
return 0;
}
int temp=n2-n1+1;
int half=temp/2;
int result1=0;
int result2=0;
int result3=0;
int result4=0;
if(temp%2==1) {
result1=dfs1(k-1,n1,n1+half-1,x,y);
result2=dfs1(k,n1+half+1,n2, x,y);
}else {
result1=dfs1(k-1,n1,n1+half-2, x, y);
result2=dfs1(k,n1+half,n2,x,y);
}
if(result1>result2) {
return 1+result1;
}else {
return 1+result2;
}
}
- 二分法是必须的,为了减少搜索的次数
下面的代码就是超时了。
public static int dfs2(int k,int n) {
if(k==1)return n;
if(n==1)return 1;
if(n==2)return 2;
if(n==3)return 2;
int result1=0;
int result2=0;
int result3=0;
int result4=0;
int mid=n/2+2;
int min=100000;
if(map.containsKey(n*100+k)) {
return map.get(n*100+k);
}else {
while(true&&mid>1) {
mid=mid-1;
result1=dfs2(k-1,mid-1);
result2=dfs2(k,n-mid);
if(result1<result2)result1=result2;
if(min>=result1) {
min=result1;
}else {
break;
}
}
map.put(n*100+k, 1+min);
}
return map.get(n*100+k);
}