这个问题是面试高频算法题,形式各式各样,鸡蛋掉落,杯子质量测试,鹰蛋硬度测试~让我们解决它吧!
你将获得 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
来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/super-egg-drop 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
那么,看题目求得是最优值,尝试使用动态规划吧~
dp[i][j]
代表i
个蛋在j
层楼内最坏情况下确定F
所用的最少次数。
dp[1][j] = j
;dp[i][0] = 0
。当鸡蛋从第x
层(0 < x < j)
扔下去时,有两种结果:
F < x
,dp[i][j] = 1 + dp[i-1][x-1]
;F >= x
,而且鸡蛋还能用,dp[i][j] = 1 + dp[i][j-x]
。因为我们要找无论F的初始值如何的条件下的查找次数,所以这两种情况的答案取最大值,而后在所有的决策中取最小值,得到下面状态转移方程:
d p [ i ] [ j ] = min { 1 + max { d p [ i − 1 ] [ x − 1 ] , d p [ i ] [ N − x ] } ∣ 1 < = x < = j } dp[i][j] = \min \lbrace 1 + \max\lbrace dp[i-1][x-1], dp[i][N-x] \rbrace | 1 <= x <= j \rbrace dp[i][j]=min{1+max{dp[i−1][x−1],dp[i][N−x]}∣1<=x<=j}
这个算法的时间复杂度是 O ( K N 2 ) O(KN^2) O(KN2),提交就超出时间限制了~
class Solution {
public:
int superEggDrop(int K, int N) {
vector<vector<int>> dp(K + 1, vector<int>(N + 1, 0));
// 1个鸡蛋
for (int j=0; j<=N; j++) {
dp[1][j] = j;
}
for (int i=2; i<=K; i++) {
for (int j=1; j<=N; j++) {
int tMin = N;
for (int x=1; x<=j; x++) {
tMin = min(tMin, 1 + max(dp[i-1][x-1], dp[i][j-x]));
}
dp[i][j] = tMin;
}
}
// for (int i=0; i<=K; i++) {
// for (int j=0; j<=N; j++) {
// cout << dp[i][j] << " ";
// }
// cout << endl;
// }
return dp[K][N];
}
};
对于输入:鸡蛋个数 K
和楼层数 N
,我们可以发现,如果鸡蛋的个数够用的话,可以直接使用二分法,在 O ( 1 ) O(1) O(1) 时间内算出 F
为 ⌈ log 2 ( N + 1 ) ⌉ \lceil \log_2(N+1) \rceil ⌈log2(N+1)⌉,而这就是最坏情况下所需要的鸡蛋个数。此时代码的时间复杂度是 O ( N 2 log N ) O(N^2\log N) O(N2logN),仍然超时~
class Solution {
public:
int superEggDrop(int K, int N) {
// 当鸡蛋个数足够,可直接算出最少次数
int temp = log(N) / log(2) + 1.0;
if (temp <= K) return (int)temp;
vector<vector<int>> dp(K + 1, vector<int>(N + 1, 0));
// 1个鸡蛋
for (int j=0; j<=N; j++) {
dp[1][j] = j;
}
for (int i=2; i<=K; i++) {
for (int j=1; j<=N; j++) {
int tMin = N;
for (int x=1; x<=j; x++) {
tMin = min(tMin, 1 + max(dp[i-1][x-1], dp[i][j-x]));
}
dp[i][j] = tMin;
}
}
return dp[K][N];
}
};
仔细观察,我们会发现:dp[i][j] >= dp[i][j-1], j >= 1
,大家可自行用归纳法证明。利用 dp[i][j]
的单调性,得到:
dp[i-1][x-1] < dp[i][j-x]
,则dp[i][j]
取值为dp[i][j-x]
,当 x' < x
时,由dp[i][j]
单调性可得,dp[i][j-x'] >= dp[i][j-x]
,x'
结果没有x
结果好,我们可以向x
右侧取值;dp[i-1][x-1] >= dp[i][j-x]
,则dp[i][j]
取值为dp[i-1][x-1]
,当 x' > x
时,由dp[i][j]
单调性可得,dp[i-1][x'-1] >= dp[i-1][x-1]
,x'
结果没有x
结果好,我们可以向x
左侧取值;如此,在状态转移过程中,如同二分查找一般,每次将 x
取值范围缩小一半,在 ⌈ log 2 ( N + 1 ) ⌉ \lceil \log_2(N+1) \rceil ⌈log2(N+1)⌉ 时间内找到最小的结果赋给dp[i][j]
。此时,算法时间复杂度是 O ( N log 2 N ) O(N \log^2 N) O(Nlog2N),终于可以AC了!
class Solution {
public:
int superEggDrop(int K, int N) {
// 当鸡蛋个数足够,可直接算出最少次数
int temp = log(N) / log(2) + 1.0;
if (temp <= K) return (int)temp;
vector<vector<int>> dp(K + 1, vector<int>(N + 1, 0));
// 1个鸡蛋
for (int j=0; j<=N; j++) {
dp[1][j] = j;
}
for (int i=2; i<=K; i++) {
for (int j=1; j<=N; j++) {
int tMin = N, left = 1, right = j;
// 二分法取得最佳决策
while (left <= right) {
int x = (left + right) / 2;
if (dp[i-1][x-1] < dp[i][j-x]) {
tMin = min(tMin, 1 + dp[i][j-x]);
left = x + 1;
} else if (dp[i-1][x-1] > dp[i][j-x]) {
tMin = min(tMin, 1 + dp[i-1][x-1]);
right = x - 1;
} else {
tMin = 1 + dp[i-1][x-1];
break;
}
}
dp[i][j] = tMin;
}
}
return dp[K][N];
}
};
状态转移方程还能优化为 O ( 1 ) O(1) O(1)时间复杂度,但是暂时没看懂,就不写了,后续不知会不会补上;-)这里就给一个链接吧:https://wenku.baidu.com/view/7c9de809581b6bd97f19ea72.html
上面的动态规划一直是对于鸡蛋个数 i
和楼层 j
进行计算,我们可以换个思路:
dp[i][j]
表示 i
个鸡蛋在 j
次尝试下可以测出的最多的层数
我们需要的结果是找出一个次数 x
,使得 dp[i][x-1] < F && dp[i][x] >= F
。
那么,当我们在第 x
层扔鸡蛋时,有两种情况:
dp[i][j]
测出的层数最多,我们当然希望 i-1
个鸡蛋在后面的 j-1
次尝试中测出的层数最多,这是一个子问题,即 dp[i-1][j-1]
;i
个鸡蛋在 j-1
次尝试中测出最多的层数,即 dp[i][j-1]
。因此,有下面的状态转移方程:
d p [ i ] [ j ] = 1 + d p [ i − 1 ] [ j − 1 ] + d p [ i ] [ j − 1 ] dp[i][j] = 1 + dp[i-1][j-1] + dp[i][j-1] dp[i][j]=1+dp[i−1][j−1]+dp[i][j−1]
它的时间复杂度有说是 O ( K log N ) O(K\log N) O(KlogN),有说是 O ( K N ) O(KN) O(KN),直观上看确实是 O ( K N ) O(KN) O(KN),但是把表打印出来,大部分是 O ( K log N ) O(K\log N) O(KlogN),N一般是遍历不完的。
class Solution {
public:
int superEggDrop(int K, int N) {
vector<vector<int>> dp(K + 1, vector<int>(N + 1, 0));
// 注意是竖着打表
for (int j=1; j<=N; j++) {
for (int i=1; i<=K; i++) {
dp[i][j] = 1 + dp[i-1][j-1] + dp[i][j-1];
if (dp[i][j-1] <= N && dp[i][j] > N) {
return j;
}
}
}
return N;
}
};