Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 783bc5c

Browse files
author
robot
committed
2 parents 98fac9a + 9fa2689 commit 783bc5c

File tree

1 file changed

+138
-83
lines changed

1 file changed

+138
-83
lines changed

‎problems/887.super-egg-drop.md

Lines changed: 138 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
## 题目地址(887. 鸡蛋掉落)
22

3-
原题地址:https://leetcode-cn.com/problems/super-egg-drop/
3+
https://leetcode-cn.com/problems/super-egg-drop/
44

55
## 题目描述
66

@@ -50,23 +50,20 @@
5050

5151
本题也是 vivo 2020 年提前批的一个笔试题。时间一个小时,一共三道题,分别是本题,合并 k 个链表,以及种花问题。
5252

53-
这道题我在很早的时候做过,也写了[题解](https://github.com/azl397985856/leetcode/blob/master/problems/887.super-egg-drop.md"887.super-egg-drop 题解")。现在看来,思路没有讲清楚。没有讲当时的思考过程还原出来,导致大家看的不太明白。今天给大家带来的是 887.super-egg-drop 题解的**重制版**。思路更清晰,讲解更透彻,如果觉得有用,那就转发在看支持一下?OK,我们来看下这道题吧。
53+
这道题我在很早的时候做过,也写了题解。现在看来,思路没有讲清楚。没有讲当时的思考过程还原出来,导致大家看的不太明白。今天给大家带来的是 887.super-egg-drop 题解的**重制版**。思路更清晰,讲解更透彻,如果觉得有用,那就转发在看支持一下?OK,我们来看下这道题吧。
5454

5555
这道题乍一看很复杂,我们不妨从几个简单的例子入手,尝试打开思路。
5656

57-
假如有 2 个鸡蛋,6 层楼。 我们应该先从哪层楼开始扔呢?想了一会,没有什么好的办法。我们来考虑使用暴力的手段
57+
为了方便描述,我将 f(i, j) 表示有 i 个鸡蛋, j 层楼,在最坏情况下,最少的次数
5858

59-
![](https://p.ipic.vip/120oh0.jpg)
60-
(图 1. 这种思路是不对的)
59+
假如有 2 个鸡蛋,6 层楼。 我们应该先从哪层楼开始扔呢?想了一会,没有什么好的办法。我们来考虑使用暴力的手段。
6160

6261
既然我不知道先从哪层楼开始扔是最优的,那我就依次模拟从第 1,第 2。。。第 6 层扔。每一层楼丢鸡蛋,都有两种可能,碎或者不碎。由于是最坏的情况,因此我们需要模拟两种情况,并取两种情况中的扔次数的较大值(较大值就是最坏情况)。 然后我们从六种扔法中选择最少次数的即可。
6362

6463
![](https://p.ipic.vip/5vz4r2.jpg)
65-
(图 2. 应该是这样的)
64+
(图1)
6665

67-
而每一次选择从第几层楼扔之后,剩下的问题似乎是一个规模变小的同样问题。嗯哼?递归?
68-
69-
为了方便描述,我将 f(i, j) 表示有 i 个鸡蛋, j 层楼,在最坏情况下,最少的次数。
66+
而每一次选择从第几层楼扔之后,剩下的问题似乎是一个规模变小的同样问题。比如选择从 i 楼扔,如果碎了,我们需要的答案就是 1 + f(k-1, i-1),如果没有碎,需要在找 [i+1, n],这其实等价于在 [1,n-i]中找。我们发现可以将问题转化为规模更小的子问题,因此不难想到递归来解决。
7067

7168
伪代码:
7269

@@ -98,9 +95,9 @@ class Solution:
9895
return ans
9996
```
10097

101-
可是如何这就结束的话,这道题也不能是 hard,而且这道题是公认难度较大的 hard 之一。
98+
可是如何这就结束的话,这道题也不能是 hard,而且这道题是公认难度较大的 hard 之一,肯定不会被这么轻松解决
10299

103-
上面的代码会 TLE,我们尝试使用记忆化递归来试一下,看能不能 AC。
100+
实际上上面的代码会 TLE,我们尝试使用记忆化递归来试一下,看能不能 AC。
104101

105102
```py
106103

@@ -121,19 +118,19 @@ class Solution:
121118
那只好 bottom-up(动态规划)啦。
122119

123120
![](https://p.ipic.vip/gnmqq1.jpg)
124-
(图 3)
121+
(图 2)
125122

126123
我将上面的过程简写成如下形式:
127124

128125
![](https://p.ipic.vip/m4ruew.jpg)
129-
(图 4)
126+
(图 3)
130127

131128
与其递归地进行这个过程,我们可以使用迭代的方式。 相比于上面的递归式,减少了栈开销。然而两者有着很多的相似之处。
132129

133130
如果说递归是用函数调用来模拟所有情况, 那么动态规划就是用表来模拟。我们知道所有的情况,无非就是 N 和 K 的所有组合,我们怎么去枚举 K 和 N 的所有组合? 当然是套两层循环啦!
134131

135132
![](https://p.ipic.vip/o91aox.jpg)
136-
(图 5. 递归 vs 迭代)
133+
(图 4. 递归 vs 迭代)
137134

138135
如上,你将 dp[i][j] 看成 superEggDrop(i, j),是不是和递归是一摸一样?
139136

@@ -142,41 +139,41 @@ class Solution:
142139
```py
143140
class Solution:
144141
def superEggDrop(self, K: int, N: int) -> int:
145-
for i in range(K + 1):
146-
for j in range(N + 1):
147-
if i == 1:
148-
dp[i][j] = j
149-
if j == 1 or j == 0:
150-
dp[i][j] == j
151-
dp[i][j] = j
152-
for k in range(1, j + 1):
153-
dp[i][j] = min(dp[i][j], max(dp[i - 1][k - 1] + 1, dp[i][j - k] + 1))
154-
return dp[K][N]
142+
dp = [[i for _ in range(K+1)] for i in range(N + 1)]
143+
for i in range(N + 1):
144+
for j in range(1, K + 1):
145+
dp[i][j] = i
146+
if j == 1:
147+
continue
148+
if i == 1 or i == 0:
149+
break
150+
for k in range(1, i + 1):
151+
dp[i][j] = min(dp[i][j], max(dp[k - 1][j-1] + 1, dp[i-k][j] + 1))
152+
return dp[N][K]
155153
```
156154

157155
值得注意的是,在这里内外循环的顺序无关紧要,并且内外循坏的顺序对我们写代码来说复杂程度也是类似的,各位客官可以随意调整内外循环的顺序。比如这样也是可以的:
158156

159157
```py
160158
class Solution:
161159
def superEggDrop(self, K: int, N: int) -> int:
162-
dp = [[0] * (K + 1) for _ in range(N + 1)]
163-
164-
for i in range(N + 1):
165-
for j in range( K + 1):
166-
if j == 1:
167-
dp[i][j] = i
168-
if i == 1 or i == 0:
169-
dp[i][j] == i
170-
dp[i][j] = i
171-
for k in range(1, i + 1):
172-
dp[i][j] = min(dp[i][j], max(dp[k - 1][j - 1] + 1, dp[i - k][j] + 1))
173-
return dp[N][K]
174-
dp = [[0] * (N + 1) for _ in range(K + 1)]
160+
dp = [[i for i in range(N+1)] for _ in range(K + 1)]
161+
for i in range(1, K + 1):
162+
for j in range(N + 1):
163+
dp[i][j] = j
164+
if i == 1:
165+
break
166+
if j == 1 or j == 0:
167+
continue
168+
for k in range(1, j + 1):
169+
dp[i][j] = min(dp[i][j], max(dp[i - 1][k - 1] + 1, dp[i][j - k] + 1))
170+
return dp[K][N]
175171
```
176172

177173
总结一下,上面的解题方法思路是:
178174

179175
![](https://p.ipic.vip/ynsszu.jpg)
176+
(图 5)
180177

181178
然而这样还是不能 AC。这正是这道题困难的地方。 **一道题目往往有不止一种状态转移方程,而不同的状态转移方程往往性能是不同的。**
182179

@@ -185,6 +182,7 @@ class Solution:
185182
把思路逆转!
186183

187184
![](https://p.ipic.vip/jtgl7i.jpg)
185+
(图 6)
188186

189187
> 这是《逆转裁判》 中经典的台词, 主角在深处绝境的时候,会突然冒出这句话,从而逆转思维,寻求突破口。
190188
@@ -197,83 +195,140 @@ class Solution:
197195
- ...
198196
- "f 函数啊 f 函数,我扔 m 次呢?", 也就是判断 f(k, m) >= N 的返回值
199197

200-
我们只需要返回第一个返回值为 true 的 m 即可。
198+
我们只需要返回第一个返回值为 true 的 m 即可。由于 m 不会大于 N,因此时间复杂度也相对可控。这么做的好处就是不用思考从哪里开始扔,扔完之后下一次从哪里扔。
199+
200+
对于这种二段性的题目应该想到二分法,如果你没想起来,请先观看我的仓库里的二分专题哦。实际上不二分也完全可以通过此题目,具体下方代码,有实现带二分的和不带二分的。
201201

202-
> 想到这里,我条件发射地想到了二分法。 聪明的小朋友们,你们觉得二分可以么?为什么?欢迎评论区留言讨论。
202+
最后剩下一个问题。这个神奇的 f 函数怎么实现呢?
203203

204-
那么这个神奇的 f 函数怎么实现呢?其实很简单。
204+
- 摔碎的情况,可以检测的最大楼层数是`f(m - 1, k - 1)`。也就是说,接下来我们需要往下找,最多可以找 f(m-1, k-1) 层
205+
- 没有摔碎的情况,可以检测的最大楼层数是`f(m - 1, k)`。也就是说,接下来我们需要往上找,最多可以找 f(m-1, k) 层
205206

206-
- 摔碎的情况,可以检测的最高楼层是`f(m - 1, k - 1) + 1`。因为碎了嘛,我们多检测了摔碎的这一层。
207-
- 没有摔碎的情况,可以检测的最高楼层是`f(m - 1, k)`。因为没有碎,也就是说我们啥都没检测出来(对能检测的最高楼层无贡献)。
207+
也就是当前扔的位置上面可以有 f(m-1, k) 层,下面可以有 f(m-1, k-1) 层,这样无论鸡蛋碎不碎,我都可以检测出来。因此能检测的最大楼层数就是**向上找的最大楼层数+向下找的最大楼层数+1**,其中 1 表示当前层,即 `f(m - 1, k - 1) + f(m - 1, k) + 1`
208208

209-
我们来看下代码:
209+
首先我们来看下二分代码:
210210

211211
```py
212212
class Solution:
213213
def superEggDrop(self, K: int, N: int) -> int:
214+
215+
@cache
214216
def f(m, k):
215217
if k == 0 or m == 0: return 0
216218
return f(m - 1, k - 1) + 1 + f(m - 1, k)
217-
m = 0
218-
while f(m, K) < N:
219-
m += 1
220-
return m
219+
l, r = 1, N
220+
while l <= r:
221+
mid = (l + r) // 2
222+
if f(mid, K) >= N:
223+
r = mid - 1
224+
else:
225+
l = mid + 1
226+
227+
return l
221228
```
222229

223-
上面的代码可以 AC。我们来顺手优化成迭代式。
224-
225-
```py
226-
class Solution:
227-
def superEggDrop(self, K: int, N: int) -> int:
228-
dp = [[0] * (K + 1) for _ in range(N + 1)]
229-
m = 0
230-
while dp[m][K] < N:
231-
m += 1
232-
for i in range(1, K + 1):
233-
dp[m][i] = dp[m - 1][i - 1] + 1 + dp[m - 1][i]
234-
return m
235-
```
230+
下面代码区我们实现不带二分的版本。
236231

237232
## 代码
238233

239-
代码支持:JavaSCript,Python
234+
代码支持:Python, CPP, Java, JavaSCript
240235

241236
Python:
242237

243238
```py
244239
class Solution:
245240
def superEggDrop(self, K: int, N: int) -> int:
246-
dp = [[0] * (K + 1) for _ in range(N + 1)]
247-
m = 0
248-
while dp[m][K] < N:
249-
m += 1
250-
for i in range(1, K + 1):
251-
dp[m][i] = dp[m - 1][i - 1] + 1 + dp[m - 1][i]
252-
return m
241+
dp = [[0] * (N + 1) for _ in range(K + 1)]
242+
243+
for m in range(1, N + 1):
244+
for k in range(1, K + 1):
245+
dp[k][m] = dp[k - 1][m - 1] + 1 + dp[k][m - 1]
246+
if dp[k][m] >= N:
247+
return m
248+
249+
return N # Fallback, should not reach here
250+
```
251+
252+
CPP:
253+
254+
```cpp
255+
#include <vector>
256+
#include <functional>
257+
258+
class Solution {
259+
public:
260+
int superEggDrop(int K, int N) {
261+
std::vector<std::vector<int>> dp(K + 1, std::vector<int>(N + 1, 0));
262+
263+
for (int m = 1; m <= N; ++m) {
264+
for (int k = 1; k <= K; ++k) {
265+
dp[k][m] = dp[k - 1][m - 1] + 1 + dp[k][m - 1];
266+
if (dp[k][m] >= N) {
267+
return m;
268+
}
269+
}
270+
}
271+
272+
return N; // Fallback, should not reach here
273+
}
274+
};
275+
276+
```
277+
278+
Java:
279+
280+
```java
281+
import java.util.Arrays;
282+
283+
class Solution {
284+
public int superEggDrop(int K, int N) {
285+
int[][] dp = new int[K + 1][N + 1];
286+
287+
for (int m = 1; m <= N; ++m) {
288+
for (int k = 1; k <= K; ++k) {
289+
dp[k][m] = dp[k - 1][m - 1] + 1 + dp[k][m - 1];
290+
if (dp[k][m] >= N) {
291+
return m;
292+
}
293+
}
294+
}
295+
296+
return N; // Fallback, should not reach here
297+
}
298+
}
299+
253300
```
254301

255302
JavaSCript:
256303

257304
```js
258-
var superEggDrop = function (K, N) {
259-
// 不选择dp[K][M]的原因是dp[M][K]可以简化操作
260-
const dp = Array(N + 1)
261-
.fill(0)
262-
.map((_) => Array(K + 1).fill(0));
263-
264-
let m = 0;
265-
while (dp[m][K] < N) {
266-
m++;
267-
for (let k = 1; k <= K; ++k) dp[m][k] = dp[m - 1][k - 1] + 1 + dp[m - 1][k];
268-
}
269-
return m;
270-
};
305+
/**
306+
* @param {number} k
307+
* @param {number} n
308+
* @return {number}
309+
*/
310+
var superEggDrop = function superEggDrop(K, N) {
311+
const dp = Array.from({ length: K + 1 }, () => Array(N + 1).fill(0));
312+
313+
for (let m = 1; m <= N; ++m) {
314+
for (let k = 1; k <= K; ++k) {
315+
dp[k][m] = dp[k - 1][m - 1] + 1 + dp[k][m - 1];
316+
if (dp[k][m] >= N) {
317+
return m;
318+
}
319+
}
320+
}
321+
322+
return N; // Fallback, should not reach here
323+
}
324+
325+
271326
```
272327

273328
**复杂度分析**
274329

275-
- 时间复杂度:$O(m * K)$,其中 m 为答案。
276-
- 空间复杂度:$O(K * N)$
330+
- 时间复杂度:$O(N * K)$
331+
- 空间复杂度:$O(N * K)$
277332

278333
对为什么用加法的同学有疑问的可以看我写的[《对《丢鸡蛋问题》的一点补充》](https://lucifer.ren/blog/2020/08/30/887.super-egg-drop-extension/)
279334

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /