Skip to content

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)