问题出处:中文版 LeetCode 第 887 题 - 鸡蛋掉落 问题
问题描述:(源自 LeetCode)
你将获得 K 个鸡蛋,并可以使用一栋从 1 到 N 共有 N 层楼的建筑。
每个蛋的功能都是一样的,如果一个蛋碎了,你就不能再把它掉下去。
你知道存在楼层 F ,满足 0 <= F <= N 任何从高于 F 的楼层落下的鸡蛋都会碎,从 F 楼层或比它低的楼层落下的鸡蛋都不会破。
每次移动,你可以取一个鸡蛋(如果你有完整的鸡蛋)并把它从任一楼层 X 扔下(满足 1 <= X <= N)。
你的目标是确切地知道 F 的值是多少。
无论 F 的初始值如何,你确定 F 的值的最小移动次数是多少?
问题理解:
给定两个参数 K个鸡蛋 和 N层楼,目标是在任何F值情况下,寻找 最小移动次数。
由于F值可以为任意值,则 最小移动次数 的下限应该取 最坏情况 下的值。
因此,问题的核心是寻找一种在 最坏情况 下的 最优方案。
由于鸡蛋的数量在寻找过程中是变化的,即某一步骤的鸡蛋数量,取决于前一步骤的鸡蛋是否破碎。
因此,尝试使用 动态规划 的方法来解决问题。
动态规划:
首先,要确定每一步之间的关系表达式,设 最小移动次数 为 ,关系表达式为 ,即 。
然后,假设第一步在 X 层掉落鸡蛋,则存在两种情况:
(1)鸡蛋破碎:剩余 K - 1 个鸡蛋,剩余需要确认的楼层数为 X - 1 层;
(2)鸡蛋完好:剩余 K 个鸡蛋,剩余需要确认的楼层数为 N - X 层;
此时,进入第二步尝试,问题已被拆分为两个子问题:
(1)给定 K - 1 个鸡蛋 和 X - 1 层楼,确定 最小移动次数,即 。
(2)给定 K 个鸡蛋 和 N - X 层楼,确定 最小移动次数,即 。
由于要找 最坏情况 下的解,因此,对于两个子问题的解,应该取较大的一个解,即
由于要找 最坏情况 下的 最优解,因此,对于 1 <= X <= N 的所有解,应该取最小的一个解,即
... ...
当寻找到上一步(子问题)的最优解时,还需要将本次移动次数考虑进去,因此,最终关系表达式为
从而,得到了该问题每一步之间的关系表达式,结合二维数组,最终解决方案代码如下:
class Solution {
public int superEggDrop(int K, int N) {
// 可直接得知答案
if (K == 1 || N == 1 || N == 2) {
return N;
}
// 创建二维数组
int[][] tempArray = new int[K + 1][N + 1];
// 初始化首行
for (int colN = 1; colN <= N; colN++) {
tempArray[1][colN] = colN;
}
// 初始化首列和第二列
for (int rowK = 1; rowK <= K; rowK++) {
tempArray[rowK][1] = 1;
tempArray[rowK][2] = 2;
}
// 从第(2,3)位置开始遍历,计算每一步的最优解。
for (int rowK = 2; rowK <= K; rowK++) {
for (int colN = 3; colN <= N; colN++) {
int minValue = Integer.MAX_VALUE;
for (int x = 1; x <= colN; x++) {
int valueAtX = Math.max(tempArray[rowK - 1][x - 1], tempArray[rowK][colN - x]);
if (valueAtX < minValue) {
minValue = valueAtX;
}
}
tempArray[rowK][colN] = minValue + 1; // 当前步骤最优解 = 上一步最优解 + 当前1步
}
}
return tempArray[K][N];
}
}
进阶优化:
通过上述方案,可以找到题目的答案,但是,无法通过 LeetCode 检测,因为耗时超过限制。
观察上述代码,可以发现,主要耗时在三次遍历循环部分,耗时将以指数型增长。
通过关系表达式,也可以发现,其中存在着最大值、最小值等连锁判断,复杂程度可想而知。
当一条路走不通,就需要考虑换一条路来试试。
在上述解决方案中,给定 N 和 K 值,设 最小移动次数 为 ,并试图寻找 的关系表达式。
若调换一下位置,假设 给定 K 和 值,能否通过寻找 的关系表达式,从而得到问题的解?
按照上述转换,问题被转化为 给定鸡蛋数量K 和 可移动次数,可以检测的 最大楼层数 为多少?
假设在第 步,从某层(假设为)掉落鸡蛋,则存在两种情况:
(1)鸡蛋破碎:剩余 K - 1 个鸡蛋,剩余可移动步数 次;
(2)鸡蛋完好:剩余 K 个鸡蛋,剩余可移动步数 次;
此时,第步的可检测最大楼层数,决定了第 步的可检测最大楼层数的范围:
(1)鸡蛋破碎时,需要利用 剩余鸡蛋 和 剩余步数 在 层 下面 的楼层检测出F值。
因此,下面的楼层数最大不能超过。
(2)鸡蛋完好时,需要利用 剩余鸡蛋 和 剩余步数 在 层 上面 的楼层检测出F值。
因此,上面的楼层数最大不能超过。
由此可知,第 步可检测出的最大楼层数 。
使 的最小 值,便是题解。
通过动态规划方法,结合二维数组表示每一步的解,行为步数 值,列为鸡蛋数 K 值;
根据关系表达式可知,每一个坐标的值等于 上一行同列值 + 上一行前一列值 + 1 (本层)。
由于 为所求值,创建二维数组时,无法确定 值,因此,以 一维数组 代替 二维数组 进行计算;
则每一个坐标的值等于 当前位置值 + 前一位置值 + 1(本层)。
至此,已完成进阶解决方案,代码如下:
class Solution {
public int superEggDrop(int K, int N) {
// 可直接给出解
if (K == 1 || N == 1 || N == 2) {
return N;
}
// 初始化首行(第一步)
int[] tempArray = new int[K + 1];
for (int index = 1; index <= K; index++) {
tempArray[index] = 1;
}
// 从第二步开始计算
int x = 2;
while (true) {
for (int k = K; k > 1; k--) { // 从后向前计算,以免 k-1 位置过早被新值覆盖,导致后续计算错误。
tempArray[k] = tempArray[k] + tempArray[k - 1] + 1;
}
// 判断是否满足条件
if (tempArray[K] >= N) {
return x;
} else {
tempArray[1] = x; // 覆盖首位值
x++; // 进入下一步
}
}
}
}
相关文章