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 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)

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