1
1
## 题目地址(887. 鸡蛋掉落)
2
2
3
- 原题地址: https://leetcode-cn.com/problems/super-egg-drop/
3
+ https://leetcode-cn.com/problems/super-egg-drop/
4
4
5
5
## 题目描述
6
6
50
50
51
51
本题也是 vivo 2020 年提前批的一个笔试题。时间一个小时,一共三道题,分别是本题,合并 k 个链表,以及种花问题。
52
52
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,我们来看下这道题吧。
54
54
55
55
这道题乍一看很复杂,我们不妨从几个简单的例子入手,尝试打开思路。
56
56
57
- 假如有 2 个鸡蛋,6 层楼。 我们应该先从哪层楼开始扔呢?想了一会,没有什么好的办法。我们来考虑使用暴力的手段 。
57
+ 为了方便描述,我将 f(i, j) 表示有 i 个鸡蛋, j 层楼,在最坏情况下,最少的次数 。
58
58
59
- ![ ] ( https://p.ipic.vip/120oh0.jpg )
60
- (图 1. 这种思路是不对的)
59
+ 假如有 2 个鸡蛋,6 层楼。 我们应该先从哪层楼开始扔呢?想了一会,没有什么好的办法。我们来考虑使用暴力的手段。
61
60
62
61
既然我不知道先从哪层楼开始扔是最优的,那我就依次模拟从第 1,第 2。。。第 6 层扔。每一层楼丢鸡蛋,都有两种可能,碎或者不碎。由于是最坏的情况,因此我们需要模拟两种情况,并取两种情况中的扔次数的较大值(较大值就是最坏情况)。 然后我们从六种扔法中选择最少次数的即可。
63
62
64
63
![ ] ( https://p.ipic.vip/5vz4r2.jpg )
65
- (图 2. 应该是这样的 )
64
+ (图1 )
66
65
67
- 而每一次选择从第几层楼扔之后,剩下的问题似乎是一个规模变小的同样问题。嗯哼?递归?
68
-
69
- 为了方便描述,我将 f(i, j) 表示有 i 个鸡蛋, j 层楼,在最坏情况下,最少的次数。
66
+ 而每一次选择从第几层楼扔之后,剩下的问题似乎是一个规模变小的同样问题。比如选择从 i 楼扔,如果碎了,我们需要的答案就是 1 + f(k-1, i-1),如果没有碎,需要在找 [ i+1, n] ,这其实等价于在 [ 1,n-i] 中找。我们发现可以将问题转化为规模更小的子问题,因此不难想到递归来解决。
70
67
71
68
伪代码:
72
69
@@ -98,9 +95,9 @@ class Solution:
98
95
return ans
99
96
```
100
97
101
- 可是如何这就结束的话,这道题也不能是 hard,而且这道题是公认难度较大的 hard 之一。
98
+ 可是如何这就结束的话,这道题也不能是 hard,而且这道题是公认难度较大的 hard 之一,肯定不会被这么轻松解决 。
102
99
103
- 上面的代码会 TLE,我们尝试使用记忆化递归来试一下,看能不能 AC。
100
+ 实际上上面的代码会 TLE,我们尝试使用记忆化递归来试一下,看能不能 AC。
104
101
105
102
``` py
106
103
@@ -121,19 +118,19 @@ class Solution:
121
118
那只好 bottom-up(动态规划)啦。
122
119
123
120
![ ] ( https://p.ipic.vip/gnmqq1.jpg )
124
- (图 3 )
121
+ (图 2 )
125
122
126
123
我将上面的过程简写成如下形式:
127
124
128
125
![ ] ( https://p.ipic.vip/m4ruew.jpg )
129
- (图 4 )
126
+ (图 3 )
130
127
131
128
与其递归地进行这个过程,我们可以使用迭代的方式。 相比于上面的递归式,减少了栈开销。然而两者有着很多的相似之处。
132
129
133
130
如果说递归是用函数调用来模拟所有情况, 那么动态规划就是用表来模拟。我们知道所有的情况,无非就是 N 和 K 的所有组合,我们怎么去枚举 K 和 N 的所有组合? 当然是套两层循环啦!
134
131
135
132
![ ] ( https://p.ipic.vip/o91aox.jpg )
136
- (图 5 . 递归 vs 迭代)
133
+ (图 4 . 递归 vs 迭代)
137
134
138
135
如上,你将 dp[ i] [ j ] 看成 superEggDrop(i, j),是不是和递归是一摸一样?
139
136
@@ -142,41 +139,41 @@ class Solution:
142
139
``` py
143
140
class Solution :
144
141
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]
155
153
```
156
154
157
155
值得注意的是,在这里内外循环的顺序无关紧要,并且内外循坏的顺序对我们写代码来说复杂程度也是类似的,各位客官可以随意调整内外循环的顺序。比如这样也是可以的:
158
156
159
157
``` py
160
158
class Solution :
161
159
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]
175
171
```
176
172
177
173
总结一下,上面的解题方法思路是:
178
174
179
175
![ ] ( https://p.ipic.vip/ynsszu.jpg )
176
+ (图 5)
180
177
181
178
然而这样还是不能 AC。这正是这道题困难的地方。 ** 一道题目往往有不止一种状态转移方程,而不同的状态转移方程往往性能是不同的。**
182
179
@@ -185,6 +182,7 @@ class Solution:
185
182
把思路逆转!
186
183
187
184
![ ] ( https://p.ipic.vip/jtgl7i.jpg )
185
+ (图 6)
188
186
189
187
> 这是《逆转裁判》 中经典的台词, 主角在深处绝境的时候,会突然冒出这句话,从而逆转思维,寻求突破口。
190
188
@@ -197,83 +195,140 @@ class Solution:
197
195
- ...
198
196
- ”f 函数啊 f 函数,我扔 m 次呢?“, 也就是判断 f(k, m) >= N 的返回值
199
197
200
- 我们只需要返回第一个返回值为 true 的 m 即可。
198
+ 我们只需要返回第一个返回值为 true 的 m 即可。由于 m 不会大于 N,因此时间复杂度也相对可控。这么做的好处就是不用思考从哪里开始扔,扔完之后下一次从哪里扔。
199
+
200
+ 对于这种二段性的题目应该想到二分法,如果你没想起来,请先观看我的仓库里的二分专题哦。实际上不二分也完全可以通过此题目,具体下方代码,有实现带二分的和不带二分的。
201
201
202
- > 想到这里,我条件发射地想到了二分法。 聪明的小朋友们,你们觉得二分可以么?为什么?欢迎评论区留言讨论。
202
+ 最后剩下一个问题。这个神奇的 f 函数怎么实现呢?
203
203
204
- 那么这个神奇的 f 函数怎么实现呢?其实很简单。
204
+ - 摔碎的情况,可以检测的最大楼层数是` f(m - 1, k - 1) ` 。也就是说,接下来我们需要往下找,最多可以找 f(m-1, k-1) 层
205
+ - 没有摔碎的情况,可以检测的最大楼层数是` f(m - 1, k) ` 。也就是说,接下来我们需要往上找,最多可以找 f(m-1, k) 层
205
206
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 `
208
208
209
- 我们来看下代码 :
209
+ 首先我们来看下二分代码 :
210
210
211
211
``` py
212
212
class Solution :
213
213
def superEggDrop (self , K : int , N : int ) -> int :
214
+
215
+ @cache
214
216
def f (m , k ):
215
217
if k == 0 or m == 0 : return 0
216
218
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
221
228
```
222
229
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
+ 下面代码区我们实现不带二分的版本。
236
231
237
232
## 代码
238
233
239
- 代码支持:JavaSCript, Python
234
+ 代码支持:Python, CPP, Java, JavaSCript
240
235
241
236
Python:
242
237
243
238
``` py
244
239
class Solution :
245
240
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
+
253
300
```
254
301
255
302
JavaSCript:
256
303
257
304
``` 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
+
271
326
```
272
327
273
328
** 复杂度分析**
274
329
275
- - 时间复杂度:$O(m * K)$,其中 m 为答案。
276
- - 空间复杂度:$O(K * N )$
330
+ - 时间复杂度:$O(N * K)$
331
+ - 空间复杂度:$O(N * K )$
277
332
278
333
对为什么用加法的同学有疑问的可以看我写的[ 《对《丢鸡蛋问题》的一点补充》] ( https://lucifer.ren/blog/2020/08/30/887.super-egg-drop-extension/ ) 。
279
334
0 commit comments