11## 题目地址(887. 鸡蛋掉落)
22
3- 原题地址: https://leetcode-cn.com/problems/super-egg-drop/
3+ https://leetcode-cn.com/problems/super-egg-drop/
44
55## 题目描述
66
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
143140class 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
160158class 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
212212class 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
241236Python:
242237
243238``` py
244239class 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
255302JavaSCript:
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