鸡蛋掉落-两枚鸡蛋 图解题解
这道题到底在问什么
- 输入
- n = 2
- 输出
- 2
- 输入
- n = 1
- 输出
- 1
- 输入
- 本节演示 n = 10
- 输出
- 4
先想最直接的笨办法
一句话套路:枚举第一枚蛋首扔的层 j,碎了往下 j-1 层线性兜底、没碎往上是 f[i-j] 子问题,两者取坏的加一,再对所有 j 取最小。f[0]=0,答案 f[n]。(动画第 3 步)
最优解:为什么这么做
一句话答案:LeetCode 1884 鸡蛋掉落-两枚鸡蛋用一维动态规划:f[i] 记 i 层最坏最少扔几次,枚举首扔层 j,碎了往下 j-1 兜底、没碎看 f[i-j],取最坏加一再对 j 取最小,时间 O(n²)、空间 O(n)。
两枚鸡蛋、n 层楼,到底在求哪个数
一栋 n 层楼有个临界楼层:比它高的层扔鸡蛋会碎,它或更低不碎。手里 2 枚一样的蛋,碎了报废、没碎捡回接着用。求最坏情况下最少扔几次能定位它。题面 n=2 答案 2、n=1 答案 1,本文用 n=10 演示、答案 4。
一枚蛋只能从第 1 层逐层试、最坏 n 次;两枚才敢让第一枚大步跨扔,碎了第二枚兜底。
把每种扔法都排开试,会炸成什么样
首扔哪层、碎了没碎各挪到哪,每步都分叉,把每种扔法全排开模拟,方案数随楼层指数级铺开,『还剩 8 层要定位』还会在不同开局里反复重算。
『楼高 i 层最少扔几次』答案固定,算一次存下来直接查,指数枚举就压成一张表——这就是动态规划(把每个小问题的答案算一遍存进 f 数组、后面直接取)。
f[i] 该记什么,第 0 格为什么是 0
f[i] 表示『楼高正好 i 层时最坏情况下最少扔几次能定位临界楼层』(状态定义,即讲清每格存什么量),答案是 f[n]。
第 0 格:楼高 0 层不用扔,f[0]=0,不用再往下拆的起点(base case)。之后每格靠左边已算好的值往右推。
碎与不碎取最坏,再对首扔层取最省
算 f[i] 试首扔在第 j 层(j 取 1 到 i)。碎了:临界层在下面 j-1 层里,第二枚只能一层层试,最坏 j-1 次。没碎:剩上面 i-j 层、两枚都在,是更小的同类问题 f[i-j] 次。
这一扔先花一次,结局不由你挑,最坏情况两条都得扛住、取更大,再对首扔层 j 取最省:f[i]=min(1+max(j-1, f[i-j])),j 取 1 到 i(转移式,即由更小的格子推出当前格)。
拿 n=10 把 f[0] 到 f[10] 逐格填一遍
先摆 f[0]=0。f[1] 只能首扔第 1 层,碎了往下 0 层、没碎看 f[0]=0,取坏加一得 f[1]=1。f[2]:首扔第 1 层 1+max(0, f[1]=1)=2,首扔第 2 层 1+max(1, f[0]=0)=2,f[2]=2。f[3] 首扔第 2 层最省,f[3]=2。f[4] 首扔前三层都是 3,f[4]=3。
往右每格照样试一圈取最小:f[5]=3、f[6]=3、f[7]=4、f[8]=4、f[9]=4、f[10]=4(首扔第 4 层:没碎接 f[6]=3、碎了兜底 3 次,取坏加一得 4)。答案 4。
碎了往下写成 j 层,f[4] 为什么会算少一次
碎了兜底是 j-1 层不是 j——首扔那层已试过,只需再排查下面 j-1 层,写成 j 就重复数首扔层、代价虚高连锁整张表全偏。括号里该取 max 写成 min 是另一处反写,等于赌每次撞好结局,可题目问最坏情况。C++、Java 哨兵(哨兵=占位用的假极大值)别用 INT_MAX,加 1 溢出成负数被 min 误选。
外层 i 从 1 到 n、内层 j 从 1 到 i,约 n²/2 次比较,时间 O(n²)(大 O 记号描述规模变大时操作数怎么涨);长 n+1 的数组 f,空间 O(n)。边界:n=0 没楼 f[0]=0,n=1 扔一次 f[1]=1,n=6 恰好 3 次(3 次最多覆盖 1+2+3=6 层,道理见下方问答)。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3一句话套路:枚举第一枚蛋首扔的层 j,碎了往下 j-1 层线性兜底、没碎往上是 f[i-j] 子问题,两者取坏的加一,再对所有 j 取最小。f[0]=0,答案 f[n]。
- 4先把地基打好。数组第 0 格代表「0 层楼」这个边界:楼都没有,临界层只能是 0,不用扔任何一次,所以 f[0] = 0。后面每一格 f[i] 都要靠它左边已经算好的这些值往右推。
- 5轮到第 1 格。要定 f[1],就把第一枚蛋的首扔层 j 从 1 试到 1:碎了往下 j-1 层线性兜底,没碎往上看子问题 f[1-j],两者取坏的加一。左边蓝色的格子都是已经算好的 f 值,可以直接查用。比一圈发现首扔在第 1 层最省。
- 6锁定 f[1] = 1。绿色那格是首扔在第 1 层、蛋没碎时落到的子问题 f[0] = 0;而蛋碎时往下 0 层线性兜底要 0 次。两条路取坏的是 0,再加首扔这一次,合计 1。这个值填进第 1 格,继续往右推。
- 7轮到第 2 格。要定 f[2],就把第一枚蛋的首扔层 j 从 1 试到 2:碎了往下 j-1 层线性兜底,没碎往上看子问题 f[2-j],两者取坏的加一。左边蓝色的格子都是已经算好的 f 值,可以直接查用。比一圈发现首扔在第 1 层最省。
- 8锁定 f[2] = 2。绿色那格是首扔在第 1 层、蛋没碎时落到的子问题 f[1] = 1;而蛋碎时往下 0 层线性兜底要 0 次。两条路取坏的是 1,再加首扔这一次,合计 2。这个值填进第 2 格,继续往右推。
- 9轮到第 3 格。要定 f[3],就把第一枚蛋的首扔层 j 从 1 试到 3:碎了往下 j-1 层线性兜底,没碎往上看子问题 f[3-j],两者取坏的加一。左边蓝色的格子都是已经算好的 f 值,可以直接查用。比一圈发现首扔在第 2 层最省。
- 10锁定 f[3] = 2。绿色那格是首扔在第 2 层、蛋没碎时落到的子问题 f[1] = 1;而蛋碎时往下 1 层线性兜底要 1 次。两条路取坏的是 1,再加首扔这一次,合计 2。这个值填进第 3 格,继续往右推。
- 11轮到第 4 格。要定 f[4],就把第一枚蛋的首扔层 j 从 1 试到 4:碎了往下 j-1 层线性兜底,没碎往上看子问题 f[4-j],两者取坏的加一。左边蓝色的格子都是已经算好的 f 值,可以直接查用。比一圈发现首扔在第 1 层最省。
- 12锁定 f[4] = 3。绿色那格是首扔在第 1 层、蛋没碎时落到的子问题 f[3] = 2;而蛋碎时往下 0 层线性兜底要 0 次。两条路取坏的是 2,再加首扔这一次,合计 3。这个值填进第 4 格,继续往右推。
- 13轮到第 5 格。要定 f[5],就把第一枚蛋的首扔层 j 从 1 试到 5:碎了往下 j-1 层线性兜底,没碎往上看子问题 f[5-j],两者取坏的加一。左边蓝色的格子都是已经算好的 f 值,可以直接查用。比一圈发现首扔在第 2 层最省。
- 14锁定 f[5] = 3。绿色那格是首扔在第 2 层、蛋没碎时落到的子问题 f[3] = 2;而蛋碎时往下 1 层线性兜底要 1 次。两条路取坏的是 2,再加首扔这一次,合计 3。这个值填进第 5 格,继续往右推。
- 15轮到第 6 格。要定 f[6],就把第一枚蛋的首扔层 j 从 1 试到 6:碎了往下 j-1 层线性兜底,没碎往上看子问题 f[6-j],两者取坏的加一。左边蓝色的格子都是已经算好的 f 值,可以直接查用。比一圈发现首扔在第 3 层最省。
- 16锁定 f[6] = 3。绿色那格是首扔在第 3 层、蛋没碎时落到的子问题 f[3] = 2;而蛋碎时往下 2 层线性兜底要 2 次。两条路取坏的是 2,再加首扔这一次,合计 3。这个值填进第 6 格,继续往右推。
- 17轮到第 7 格。要定 f[7],就把第一枚蛋的首扔层 j 从 1 试到 7:碎了往下 j-1 层线性兜底,没碎往上看子问题 f[7-j],两者取坏的加一。左边蓝色的格子都是已经算好的 f 值,可以直接查用。比一圈发现首扔在第 1 层最省。
- 18锁定 f[7] = 4。绿色那格是首扔在第 1 层、蛋没碎时落到的子问题 f[6] = 3;而蛋碎时往下 0 层线性兜底要 0 次。两条路取坏的是 3,再加首扔这一次,合计 4。这个值填进第 7 格,继续往右推。
- 19轮到第 8 格。要定 f[8],就把第一枚蛋的首扔层 j 从 1 试到 8:碎了往下 j-1 层线性兜底,没碎往上看子问题 f[8-j],两者取坏的加一。左边蓝色的格子都是已经算好的 f 值,可以直接查用。比一圈发现首扔在第 2 层最省。
- 20锁定 f[8] = 4。绿色那格是首扔在第 2 层、蛋没碎时落到的子问题 f[6] = 3;而蛋碎时往下 1 层线性兜底要 1 次。两条路取坏的是 3,再加首扔这一次,合计 4。这个值填进第 8 格,继续往右推。
- 21轮到第 9 格。要定 f[9],就把第一枚蛋的首扔层 j 从 1 试到 9:碎了往下 j-1 层线性兜底,没碎往上看子问题 f[9-j],两者取坏的加一。左边蓝色的格子都是已经算好的 f 值,可以直接查用。比一圈发现首扔在第 3 层最省。
- 22锁定 f[9] = 4。绿色那格是首扔在第 3 层、蛋没碎时落到的子问题 f[6] = 3;而蛋碎时往下 2 层线性兜底要 2 次。两条路取坏的是 3,再加首扔这一次,合计 4。这个值填进第 9 格,继续往右推。
- 23轮到第 10 格。要定 f[10],就把第一枚蛋的首扔层 j 从 1 试到 10:碎了往下 j-1 层线性兜底,没碎往上看子问题 f[10-j],两者取坏的加一。左边蓝色的格子都是已经算好的 f 值,可以直接查用。比一圈发现首扔在第 4 层最省。
- 24锁定 f[10] = 4。绿色那格是首扔在第 4 层、蛋没碎时落到的子问题 f[6] = 3;而蛋碎时往下 3 层线性兜底要 3 次。两条路取坏的是 3,再加首扔这一次,合计 4。这个值填进第 10 格,继续往右推。
- 25整张表填满了,答案 f[10] = 4。绿色高亮的 1、3、6、10 是 f 值每上一个台阶的临界楼层,它们恰好是三角形数 1、1+2、1+2+3、1+2+3+4。含义很直白:允许扔 k 次时,第一枚蛋依次跨 k、k-1 一直到 1 层,最多能覆盖 k 加到 1 也就是 k 乘 k 加 1 除以 2 层楼。10 正好等于 4 乘 5 除以 2,所以 4 次够用,这也解释了为什么 n = 10 的答案就是 4。
⚠️ 容易写错的地方
✗ 错:蛋碎时把往下的兜底次数写成 j 层
✓ 对:碎了只需在下面 j-1 层里线性试
首扔在第 j 层,碎了说明临界层在 1 到 j-1 这 j-1 层里,兜底是 j-1 次不是 j 次;多算一层会让答案偏大
✗ 错:两个分支取小的那个 min(j-1, f[i-j])
✓ 对:要取坏的那个 max
f[i] 求的是最坏情况下的保证次数,必须按两种结果里更费的那条路算,所以是 max 不是 min
✗ 错:C++ / Java 用整型最大值当哨兵
✓ 对:用 1 左移 29 位或 memset 0x3f 这类留余量的大数
INT_MAX 再加 1 会溢出成负数,反而被 min 当成「最省」选走,把答案带偏;Python 的浮点 inf 无此问题
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def twoEggDrop(self, n: int) -> int:
f = [0] + [inf] * n
for i in range(1, n + 1):
for j in range(1, i + 1):
f[i] = min(f[i], 1 + max(j - 1, f[i - j]))
return f[n]C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int twoEggDrop(int n) {
int f[n + 1];
memset(f, 0x3f, sizeof(f));
f[0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i; j++) {
f[i] = min(f[i], 1 + max(j - 1, f[i - j]));
}
}
return f[n];
}
};Java
import java.util.*;
class Solution {
public int twoEggDrop(int n) {
int[] f = new int[n + 1];
Arrays.fill(f, 1 << 29);
f[0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i; j++) {
f[i] = Math.min(f[i], 1 + Math.max(j - 1, f[i - j]));
}
}
return f[n];
}
}复杂度
时间
O(n²)
外层枚举楼层 i 从 1 到 n,内层枚举首扔层 j 从 1 到 i,总比较次数约 n² 的一半,是平方级
空间
O(n)
按峰值算:只用一个长度 n+1 的一维数组 f,没有开二维表,所以是线性空间
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 鸡蛋掉落-两枚鸡蛋 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么两分支要先取 max 再对 j 取 min,不能都取 min 或都取 max?+
取 max 和取 min 管两件不同的事。首扔第 j 层后蛋碎不碎不由你控制,题目要的是最坏情况的次数,所以两种结局里必须按更费次数的那个算,取 max、保证最坏也够用。而首扔层是你能自己挑的,当然挑总代价最小的那层,所以对 j 取 min。都取 min 等于假设每次都走运结局,答案会算小;都取 max 等于连挑哪层都往最坏挑,又会算大。一里一外正好对应『结局不可控、扔法可控』。
有没有比 O(n²) 更快的做法?+
有。观察填出来的表,f 值每往上跳一个台阶的临界楼层恰好是三角形数 1、3、6、10……也就是允许扔 k 次时最多能覆盖 1+2+…+k=k(k+1)/2 层楼。于是求最少次数就是求最小的 k 让 k(k+1)/2 不小于 n,直接解这个不等式,用求根公式能 O(1) 或 O(√n) 算出,不必填整张表。动态规划版胜在直观、好推广到更多蛋,数学捷径胜在快。
推广到 k 枚鸡蛋(LeetCode 887)怎么做?+
那是 LeetCode 887 鸡蛋掉落,把蛋数也作为一维。定义 dp[k][i] 为 k 枚蛋、i 层楼的最少次数,首扔一层后碎了蛋数减一、楼层归到下面,没碎蛋数不变、楼层归到上面,转移 dp[k][i]=min(1+max(dp[k-1][j-1], dp[k][i-j]))。本题就是两枚蛋的特例。朴素二维 DP 是 O(k·n²),还能换成『k 枚蛋扔 t 次最多覆盖多少层』的对偶思路进一步压低。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 鸡蛋掉落-两枚鸡蛋 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。