LeetCode 887. 鸡蛋掉落
给你 k 枚相同的鸡蛋,并可以使用一栋从第 1 层到第 n 层共有 n 层楼的建筑。
已知存在楼层 f ,满足 0 <= f <= n ,任何从 高于 f 的楼层落下的鸡蛋都会碎,从 f 楼层或比它低的楼层落下的鸡蛋都不会破。
每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。
请你计算并返回要确定 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 <= 104
思路一:二分
-
题目中给了我们 K 个鸡蛋,以及 N 层的一栋大楼。对于鸡蛋来说,有一个临界点 f ,在楼层 f 之上扔鸡蛋的话,鸡蛋会碎。在 f 层之下扔鸡蛋则不会碎。需要我们求出找到这个零界点所需的最小操作数。
-
刚看到题时的第一个思路就是二分,时间复杂度 O(logN),看起来很美好,可是在题目中限制了鸡蛋的个数。如果鸡蛋的个数比二分需要的鸡蛋数要多,使用二分自然没问题。可是如果鸡蛋的数目比二分小的话就不能直接使用二分来做,需要我们每次操作尽可能最优 。
-
这是就有了另外一个思路,枚举所有的情况,选择最优的!
思路二:枚举
- 鸡蛋在每一层楼扔下去都有碎 or 完好两种情况,即我们需要枚举每次人鸡蛋的情况,对于每次鸡蛋碎 or 完好的情况下再次枚举其他情况,直到枚举完所有情况。
- 直接暴力枚举的话,很明显复杂度是阶乘级别的,所以必然是需要优化一下的 --> 剪枝
- 既然直接搜索枚举用不了,而且枚举的时候可以分成若干个小情况,每种小情况的最优解是唯一的,然后通过小的最优解求出最终的最优解。所以可以考虑动态规划
思路三: 动态规划
-
对于题目中的描述,有 k,n 两个变量与之有关,可以使用二维的 dp
dp[i][j] /表示已经扔了 i 个鸡蛋,当前在前 j 层扔鸡蛋的最小扔鸡蛋次数 --> dp 数组可初始化为一个极大值
-
所以需要枚举 1 ~ j 层楼的情况,找出最优解,状态转移方程如下
//假设当前在第 x 层楼扔鸡蛋, 1 <= x <= j dp[i][j] = Min(dp[i][j], Max(dp[i - 1][x - 1], dp[i][j - x]) + 1); dp[i - 1][x - 1]//x 楼扔鸡蛋,鸡蛋碎了,所以需要前 i - 1 个鸡蛋在前 x - 1 层楼的最小操作数 dp[i][j - x]// x 楼扔鸡蛋,鸡蛋完好,需要前 i 个鸡蛋在x 楼之后的 j - x 层楼的最小操作数
-
dp 数组初始化
dp[i][0] = 0;//第 0 层楼不需要扔,结果为 0 dp[1][j] = j;//只有一个鸡蛋的情况下,只能找到最优的情况扔,需要每层楼都扔一次
-
JS 代码
var superEggDrop = function(k, n) { let dp = new Array(k + 1).fill(Infinity).map(() => new Array(n + 1).fill(Infinity)); for (let i = 1; i <= n; i++) dp[1][i] = i; for (let i = 0; i <= k; i++) dp[i][0] = 0; for (let i = 2; i <= k; i++) {//鸡蛋数 for (let j = 1; j <= n; j++) {//楼层总数 for (let l = 1; l <= j; l++) {//枚举扔鸡蛋的楼层 dp[i][j] = Math.min(dp[i][j], Math.max(dp[i - 1][l - 1], dp[i][j - l]) + 1);//鸡蛋 碎 没碎 } } } return dp[k][n]; };
使用二分优化动态规划的时间复杂度
-
此解法的时间复杂度为 (kn^2),而且对于
dp[i - 1][x - 1]
来说随着 x 的增加,其值为不递减的状态(对应:鸡蛋碎 or 不碎两种情况)。对于dp[i][j - x]
来说,随着 x 的增加,其值为不递增的状态。因为,我们需要找到的是最小化的最大值,也就是说需要找到dp[i - 1][x - 1] 和 dp[i][j - x]
相等时的临界情况,或者距离相等最近的临界情况 -
由于
dp[i][j]
不递减,也就是说这个数组是有序的,所以可以使用二分搜索来找到这个临界情况,这样的话,算法的时间复杂度就变为了 (knlogn),具体 JS 代码如下var superEggDrop = function(k, n) { let dp = new Array(k + 1).fill(Infinity).map(() => new Array(n + 1).fill(Infinity)); for (let i = 1; i <= n; i++) dp[1][i] = i; for (let i = 0; i <= k; i++) dp[i][0] = 0; for (let i = 2; i <= k; i++) {//鸡蛋树 for (let j = 1; j <= n; j++) {//楼层总数 // for (let l = 1; l <= j; l++) {//枚举扔鸡蛋的楼层 // dp[i][j] = Math.min(dp[i][j], Math.max(dp[i - 1][l - 1], dp[i][j - l]) + 1);//鸡蛋 碎 没碎 --> 找一个最大的较小值 --> 两者单调性一个递增,一个递减,找他们相等时的边界条件 --> 二分 // } let [l, r] = [1, j];//扔鸡蛋的楼层 while (l < r) { const mid = (l + r) >> 1; if (dp[i - 1][mid - 1] >= dp[i][j - mid]) r = mid; else l = mid + 1; } dp[i][j] = Math.min(dp[i][j], Math.max(dp[i - 1][l - 1], dp[i][j - l]) + 1); } } return dp[k][n]; };
-
由于 dp[i] 只与 dp[i - 1] 有关,所以可以使用滚动数组优化的方式将空间复杂度由 (n^2) 降为 (n), 使用 2 * (n + 1) 的数组,0 为 i - 1,1 为 即可。在每次最内层循环结束时,将 1 移动到 0,再初始化 1。
-
除此之外,还有数学上的解法,可以参考博客开始部分的 附上大佬的题解