鸡蛋掉落问题
链接: https://leetcode-cn.com/problems/super-egg-drop/solution/ji-dan-diao-luo-by-leetcode/
题干
你将获得 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
分析
化繁就简,我们先不考虑鸡蛋的数量,给定楼层N,得出安全层F,最简单的解法就是二分法。
不妨设N=16,F=2,即一共16层楼,安全层在第2层。
具体思路是:
Step1:(1+16)/2 = 8,即在1-16层楼的中间楼层,第8层扔鸡蛋,破了,说明F在1-8层。
Step2:(1+8)/2 =4,即在1-8层的中间楼层,第4层扔鸡蛋,破了,说明F在1-3层。
Step3:(1+3)/2 =2,即在1-3层的中间楼层,第2层扔鸡蛋,没破,说明F在2-3层。
Step4:在第3层扔鸡蛋,破了,说明F=2,即安全层是第2层。
以上就是不考虑鸡蛋数量的二分法思路。那让我们加上鸡蛋的限制,本题就有了鸡蛋数量和未知楼层数两个变量,可以用动态规划的思路求解,把鸡蛋和楼层当做状态输入,得出最小移动次数。状态可以表示成 (K, N): K 为鸡蛋数和 N 为楼层数。当从第 X 楼扔鸡蛋的时候,要么鸡蛋不碎,状态变成 (K, N-X),或者碎掉,状态变成 (K-1, X-1)。
本题需要找到最小移动次数,所以最外层是min,同时,无论F初始值为多少,即考虑到最坏情况,所以里面是max,意思要是鸡蛋摔坏所需要的次数更多,那就拿它摔坏来考虑。
具体实现
// 根据提示N和K至少相差100倍,且N最大不过10000。
// 可以通过N*100+K把两个值存在一个int里,且相互独立。
// 如 N=5321,K=82,N*100+K= 542182,前四位是N的值,后两位是K的值,
// 共同组成一个key,只占用一个int数。
if (!memo.containsKey(N * 100 + K)) {
int ans;
// 未知楼层为0则不用扔了。
if (N == 0)
ans = 0;
// 只有一个鸡蛋,只能老老实实从下往上扔,
// 最坏情况是F=N,要扔N次。
else if (K == 1)
ans = N;
else {
// low和high分别代表上下两个光标,框住的这部分代表未知楼层。
int low = 1, high = N;
// 当这两个光标贴在一起的时候,代表已经穷尽所有楼层。
while (low + 1 < high) {
// 二分法查找
int x = (low + hi) / 2;
// t1和t2分别代表摔坏和没摔坏两种情况所需要的最小移动次数
int t1 = dp(K-1, x-1);
int t2 = dp(K, N-x);
//考虑最坏情况,所以选t1和t2中较大的那个情况。
if (t1 < t2)
low = x;
else if (t1 > t2)
high = x;
else
low = high = x;
}
// 比较从low这一层开始扔和从high这一层开始扔哪个次数更少。
ans = 1 + Math.min(Math.max(dp(K-1, low-1), dp(K, N-low)),
Math.max(dp(K-1, high -1), dp(K, N-high)));
}
// 无论哪种扔法,都可能出现重复的状态,所以存在memo里面以便复用。
memo.put(N * 100 + K, ans);
}
return memo.get(N * 100 + K);
}
}