Skip to content

Commit 12fccba

Browse files
committed
Create 0887. 鸡蛋掉落.md
1 parent 7aacbc3 commit 12fccba

File tree

1 file changed

+226
-0
lines changed

1 file changed

+226
-0
lines changed

Solutions/0887. 鸡蛋掉落.md

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
# [0887. 鸡蛋掉落](https://leetcode.cn/problems/super-egg-drop/)
2+
3+
- 标签:数学、二分查找、动态规划
4+
- 难度:困难
5+
6+
## 题目大意
7+
8+
**描述**:给定一个整数 `k` 和整数 `n`,分别代表 `k` 枚鸡蛋和可以使用的一栋从第 `1` 层到第 `n` 层楼的建筑。
9+
10+
已知存在楼层 `f`,满足 `0 <= f <= n`,任何从高于 `f` 的楼层落下的鸡蛋都会碎,从 `f` 楼层或比它低的楼层落下的鸡蛋都不会碎。
11+
12+
每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 `x` 扔下(满足 `1 <= x <= n`),如果鸡蛋碎了,就不能再次使用它。如果
13+
14+
**要求**:计算并返回要确定 `f` 确切值的最小操作次数是多少。
15+
16+
**说明**
17+
18+
- $1 \le k \le 100$。
19+
- $1 \le n \le 10^4$。
20+
21+
**示例**
22+
23+
```Python
24+
输入:k = 1, n = 2
25+
输入:2
26+
解释:鸡蛋从 1 楼掉落。如果它碎了,肯定能得出 f = 0。否则,鸡蛋从 2 楼掉落。如果它碎了,肯定能得出 f = 1。如果它没碎,那么肯定能得出 f = 2。因此,在最坏的情况下我们需要移动 2 次以确定 f 是多少。
27+
```
28+
29+
## 解题思路
30+
31+
### 思路 1:动态规划(超时)
32+
33+
这道题目的题意不是很容易理解,我们先把题目简化一下,忽略一些限制条件,理解简单情况下的题意。然后再一步步增加限制条件,从而先弄明白这道题目的意思。
34+
35+
我们先忽略 `k` 个鸡蛋这个条件,假设有无限个鸡蛋。
36+
37+
现在有 `1` ~ `n` 一共 `n` 层楼。已知存在楼层 `f`,低于等于 `f` 层的楼层扔下去的鸡蛋都不会碎,高于 `f` 的楼层扔下去的鸡蛋都会碎。
38+
39+
当然这个楼层 `f` 的确切值题目没有给出,需要我们一次次去测试鸡蛋最高会在哪一层不会摔碎。
40+
41+
在每次操作中,我们可以选定一个楼层,将鸡蛋扔下去:
42+
43+
- 如果鸡蛋没摔碎,则可以继续选择其他楼层进行测试。
44+
- 如果鸡蛋摔碎了,则该鸡蛋无法继续测试。
45+
46+
现在题目要求:一共有 `n` 层楼,无限个鸡蛋,求出至少需要扔几次鸡蛋,才能保证无论 `f` 是多少层,都能将 `f` 找出来?
47+
48+
最简单且直观的想法:
49+
50+
1. 从第 `1` 楼开始扔鸡蛋。`1` 楼不碎,再去 `2` 楼扔。
51+
2. `2` 楼还不碎,就去 `3` 楼扔。
52+
3. ……
53+
4. 直到鸡蛋碎了,也就找到了鸡蛋不会摔碎的最高层 `f`
54+
55+
用这种方法,最坏情况下,鸡蛋在第 `n` 层也没摔碎。这种情况下我们总共试了 `n` 次才确定鸡蛋不会摔碎的最高楼层 `f`
56+
57+
下面再来说一下比 `n` 次要少的情况。
58+
59+
如果我们可以通过二分查找的方法,先从 `1` ~ `n` 层的中间层开始扔鸡蛋。
60+
61+
- 如果鸡蛋碎了,则从第 `1` 层到中间层这个区间中去扔鸡蛋。
62+
- 如果鸡蛋没碎,则从中间层到第 `n` 层这个区间中去扔鸡蛋。
63+
64+
每次扔鸡蛋都从区间的中间层去扔,这样每次都能排除当前区间一半的答案,从而最终确定鸡蛋不会摔碎的最高楼层 `f`。。
65+
66+
通过这种二分查找的方法,可以优化到 $\log n$ 次就能确定鸡蛋不贵摔碎的最高楼层 `f`
67+
68+
因为 $\log n \le n$,所以通过二分查找的方式,「至少」比线性查找的次数要少。
69+
70+
同样,我们还可以通过三分查找、五分查找等等方式减少次数。
71+
72+
这是不限制鸡蛋个数的情况下,现在在给定 `n` 层楼的基础上,再限制一下鸡蛋个数为 `k`
73+
74+
现在题目要求:一共有 `n` 层楼,`k` 个鸡蛋,求出至少需要扔几次鸡蛋,才能保证无论 `f` 是多少层,都能将 `f` 找出来?
75+
76+
如果鸡蛋足够多(大于等于 $\log_2 n$ 个),可以通过二分查找的方法来测试。如果鸡蛋不够多,可能二分查找过程中,鸡蛋就用没了,则不能通过二分查找的方法来测试。
77+
78+
那么这时候为了找出 `f` ,我们应该如何求出最少的扔鸡蛋次数。
79+
80+
可以这样考虑。题目限定了 `n` 层楼,`k` 个鸡蛋。
81+
82+
如果我们尝试在 `1` ~ `n` 层中的任意一层 `x` 扔鸡蛋:
83+
84+
1. 如果鸡蛋没碎,则说明 `1` ~ `x` 层都不用再考虑了,我们需要用 `k` 个鸡蛋去考虑剩下的 `n - x` 层,问题就从 `(n, k)` 转变为了 `(n - x, k)`
85+
2. 如果鸡蛋碎了,则说明 `x + 1` ~ `n` 层都不用再考虑了,我们需要去剩下的 `k - 1` 个鸡蛋考虑剩下的 `x - 1` 层,问题就从 `(n, k)` 转变为了 `(x - 1, k - 1)`
86+
87+
这样一来,我们就可以根据上述关系使用动态规划方法来解决这道题目了。具体步骤如下:
88+
89+
###### 1. 划分阶段
90+
91+
按照楼层数量、剩余鸡蛋个数进行阶段划分。
92+
93+
###### 2. 定义状态
94+
95+
定义状态 `dp[i][j]` 表示为:一共有 `i` 层楼,`j` 个鸡蛋的条件下,为了找出 `f` ,最坏情况下的最少扔鸡蛋次数。
96+
97+
###### 3. 状态转移方程
98+
99+
根据之前的描述,`dp[i][j]` 有两个来源,其状态转移方程为:
100+
101+
$dp[i][j] = min_{1 \le x \le n} (max(dp[i - x][j], dp[x - 1][j - 1])) + 1$
102+
103+
###### 4. 初始条件
104+
105+
给定鸡蛋 `k` 的取值范围为 `[1, 100]``f` 值取值范围为 `[0, n]`,初始化时,可以考虑将所有值设置为当前拥有的楼层数。
106+
107+
- 当鸡蛋数为 `1` 时,`dp[i][1] = i`。这是如果唯一的蛋碎了,则无法测试了。只能从低到高,一步步进行测试,最终最少测试数为当前拥有的楼层数。
108+
- 如果刚开始初始化时已经将所有值设置为当前拥有的楼层数,则这一步可省略。
109+
- 当楼层为 `1` 时,在 `1` 层扔鸡蛋,`dp[1][j] = 1`。这是因为:
110+
- 如果在 `1` 层扔鸡蛋碎了,则 `f < 1`。同时因为 `f` 的取值范围为 `[0, n]`。所以能确定 `f = 0`。
111+
- 如果在 `1` 层扔鸡蛋没碎,则 `f >= 1`。同时因为 `f` 的取值范围为 `[0, n]`。所以能确定 `f = 0`。
112+
113+
###### 5. 最终结果
114+
115+
根据我们之前定义的状态,`dp[i][j]` 表示为:一共有 `i` 层楼,`j` 个鸡蛋的条件下,为了找出 `f` ,最坏情况下的最少扔鸡蛋次数。则最终结果为 `dp[n][k]`
116+
117+
### 思路 1:代码
118+
119+
```Python
120+
class Solution:
121+
def superEggDrop(self, k: int, n: int) -> int:
122+
dp = [[0 for _ in range(k + 1)] for i in range(n + 1)]
123+
124+
for i in range(1, n + 1):
125+
for j in range(1, k + 1):
126+
dp[i][j] = i
127+
128+
# for i in range(1, n + 1):
129+
# dp[i][1] = i
130+
131+
for j in range(1, k + 1):
132+
dp[1][j] = 1
133+
134+
for i in range(2, n + 1):
135+
for j in range(2, k + 1):
136+
for x in range(1, i + 1):
137+
dp[i][j] = min(dp[i][j], max(dp[i - x][j], dp[x - 1][j - 1]) + 1)
138+
139+
return dp[n][k]
140+
```
141+
142+
### 思路 1:复杂度分析
143+
144+
- **时间复杂度**:$O(n^2 \times k)$。三重循环的时间复杂度为 $O(n^2 \times k)$。
145+
- **空间复杂度**:$O(n \times k)$。
146+
147+
### 思路 2:动态规划优化
148+
149+
上一步中时间复杂度为 $O(n^2 \times k)$。根据 $n$ 的规模,提交上去补出意外的超时了。
150+
151+
我们可以观察一下上面的状态转移方程 $dp[i][j] = min_{1 \le x \le n} (max(dp[i - x][j], dp[x - 1][j - 1])) + 1$ 。
152+
153+
这里最外两层循环的 `i``j` 分别为状态的阶段,可以先将 `i``j` 看作固定值。最里层循环的 `x` 代表选择的任意一层 `x` ,值从 `1` 遍历到 `i`
154+
155+
此时我们把 `dp[i - x][j]``dp[x - 1][j - 1]` 分别单独来看。可以看出:
156+
157+
- 对于 `dp[i - x][j]`:当 `x` 增加时,`i - x` 的值减少,`dp[i - x][j]` 的值跟着减小。自变量 `x` 与函数 `dp[i - x][j]` 是一条单调非递增函数。
158+
- 对于 `dp[x - 1][j - 1]`:当 `x` 增加时, `x - 1` 的值增加,`dp[x - 1][j - 1]` 的值跟着增加。自变量 `x` 与函数 `dp[x - 1][j - 1]` 是一条单调非递减函数。
159+
160+
两条函数的交点处就是两个函数较大值的最小值位置。即 `dp[i][j]` 所取位置。而这个位置可以通过二分查找满足 `dp[x - 1][j - 1] >= dp[i - x][j]` 最大的那个 `x`。这样时间复杂度就从 $O(n^2 \times k)$ 优化到了 $O(n \log n \times k)$。
161+
162+
### 思路 2:代码
163+
164+
```Python
165+
class Solution:
166+
def superEggDrop(self, k: int, n: int) -> int:
167+
dp = [[0 for _ in range(k + 1)] for i in range(n + 1)]
168+
169+
for i in range(1, n + 1):
170+
for j in range(1, k + 1):
171+
dp[i][j] = i
172+
173+
# for i in range(1, n + 1):
174+
# dp[i][1] = i
175+
176+
for j in range(1, k + 1):
177+
dp[1][j] = 1
178+
179+
for i in range(2, n + 1):
180+
for j in range(2, k + 1):
181+
left, right = 1, i
182+
while left < right:
183+
mid = left + (right - left) // 2
184+
if dp[mid - 1][j - 1] < dp[i - mid][j]:
185+
left = mid + 1
186+
else:
187+
right = mid
188+
dp[i][j] = max(dp[left - 1][j - 1], dp[i - left][j]) + 1
189+
190+
return dp[n][k]
191+
```
192+
193+
### 思路 2:复杂度分析
194+
195+
- **时间复杂度**:$O(n \log n \times k)$。两重循环的时间复杂度为 $O(n \times k)$,二分查找的时间复杂度为 $O(\log n)$。
196+
- **空间复杂度**:$O(n \times k)$。
197+
198+
### 思路 3:动态规划 + 逆向思维
199+
200+
201+
202+
### 思路 3:代码
203+
204+
```Python
205+
class Solution:
206+
def superEggDrop(self, k: int, n: int) -> int:
207+
dp = [[i for _ in range(k + 1)] for i in range(n + 1)]
208+
dp[1][1] = 1
209+
210+
for i in range(1, n + 1):
211+
for j in range(1, k + 1):
212+
dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1] + 1
213+
if j == k and dp[i][j] >= n:
214+
return i
215+
return n
216+
```
217+
218+
### 思路 3:复杂度分析
219+
220+
- **时间复杂度**:$O(n \times k)$。两重循环的时间复杂度为 $O(n \times k)$。
221+
- **空间复杂度**:$O(n \times k)$。
222+
223+
## 参考资料
224+
225+
- 【题解】[题目理解 + 基本解法 + 进阶解法 - 鸡蛋掉落 - 力扣](https://leetcode.cn/problems/super-egg-drop/solution/ji-ben-dong-tai-gui-hua-jie-fa-by-labuladong/)
226+
- 【题解】[动态规划(只解释官方题解方法一)(Java) - 鸡蛋掉落 - 力扣](https://leetcode.cn/problems/super-egg-drop/solution/dong-tai-gui-hua-zhi-jie-shi-guan-fang-ti-jie-fang/)

0 commit comments

Comments
 (0)