高楼扔球问题/鸡蛋掉落问题
你面前有一栋 100 层的楼,你手上有两个完全一样的玻璃弹珠。从某个楼层往下扔,要么碎,要么不碎,且楼层越高越容易碎。现在要找到那个"刚好会碎"的临界楼层,你觉得怎么样能够最快地找到?
二分法的核心前提是你有足够多 的弹珠可以挥霍,但现在你手里只有2个。你可以推演一下用二分法最坏的情况:
- 直接折半: 你拿着第一个球跑到中间的 50 楼扔下去。
- 万一碎了: 糟了,你现在只剩最后 1 个球。为了绝对安全地找出临界点,你没有任何捷径可走,只能拿着这根"独苗"从 1 楼、2 楼、3 楼......一层一层慢慢试。
- 最坏结果: 如果临界层刚好在 49 楼,你需要扔 1次(50楼) + 49次(1到49楼) = 50次。
最坏情况下,保证能找到临界楼层的最少次数是 14次。
核心原理是让每一次尝试的最坏情况耗时都保持一致。第一个球用来"大范围跳跃"锁定区间,第二个球用来在区间内"一层层排查"。
具体策略如下:
思路不是每次固定隔相同楼层,而是让第一次弹珠的试验间隔逐渐减小。
因为如果第一颗弹珠在某层碎了,第二颗弹珠就只能从上一次安全楼层开始,一层一层往上试。
所以我们希望无论第一颗在哪次碎,总尝试次数都不超过同一个上限。
设最多允许扔 nnn 次,那么第一颗弹珠可以这样扔:
- 第 1 次:第 nnn 层
- 第 2 次:再上 n−1n-1n−1 层
- 第 3 次:再上 n−2n-2n−2 层
- ......
总共能覆盖:
n+(n−1)+(n−2)+⋯+1=n(n+1)2 n+(n-1)+(n-2)+\cdots+1=\frac{n(n+1)}{2} n+(n−1)+(n−2)+⋯+1=2n(n+1)
要求覆盖 100 层:
n(n+1)2≥100 \frac{n(n+1)}{2}\ge 100 2n(n+1)≥100
试一下:
13×142=91<100 \frac{13\times14}{2}=91<100 213×14=91<100
14×152=105≥100 \frac{14\times15}{2}=105\ge100 214×15=105≥100
所以最少需要:
14 \boxed{14} 14
具体扔法是第一颗弹珠依次从这些楼层扔:
14, 27, 39, 50, 60, 69, 77, 84, 90, 95, 99, 100 14,\ 27,\ 39,\ 50,\ 60,\ 69,\ 77,\ 84,\ 90,\ 95,\ 99,\ 100 14, 27, 39, 50, 60, 69, 77, 84, 90, 95, 99, 100
例如第一颗在 50 楼碎了,而 39 楼没碎,那么就用第二颗从 40 楼开始:
40,41,42,...,49 40,41,42,\dots,49 40,41,42,...,49
逐层测试。
为什么这是最优的?因为如果想在 13 次内完成,最多只能覆盖:
13+12+⋯+1=91 13+12+\cdots+1=91 13+12+⋯+1=91
层,不足 100 层。
所以最坏情况下的最优答案是:
14 次 \boxed{14\text{ 次}} 14 次
为什么不是固定扔?例如固定 10 层扔一次?
因为"固定每 10 层扔一次"当然也能找到答案,但它的最坏情况次数更多。
比如你这样扔第一颗:
10、20、30、40、......、100。
假设第一颗在 100 楼才碎,那前面已经扔了 10 次。接着第二颗还要从 91 楼开始逐层试:
91、92、93、......、99。
最坏还要再扔 9 次。
所以总共最多:
10+9=1910+9=1910+9=19
次。
而"14、27、39、50......"这种方法,间隔是逐渐缩小的:
14,13,12,11,...14,13,12,11,\dots14,13,12,11,...
这样做的目的是把不同情况下的最坏次数"拉平"。
比如:
- 如果第一次就在 14 楼碎了,那么已经扔 1 次,第二颗最多还要试 13 次,总共 14 次。
- 如果在 27 楼碎了,那么第一颗已经扔 2 次,第二颗最多从 15 到 26 试 12 次,总共还是 14 次。
- 如果在 39 楼碎了,第一颗用了 3 次,第二颗最多再试 11 次,总共仍然是 14 次。
也就是始终保持:
1+13=2+12=3+11=⋯=141+13=2+12=3+11=\cdots=141+13=2+12=3+11=⋯=14
这就是为什么不能固定间隔。
固定间隔的问题在于:第一颗越晚碎,前面已经消耗的次数越多,但第二颗需要逐层试的范围却没有变小。
而最优策略会让"第一颗多试一次"时,"第二颗最多需要补试的楼层数"刚好少一层,所以最坏情况能压到:
14\boxed{14}14
而固定每 10 层一次的策略,最坏要:
19\boxed{19}19
次。
C++ 实现
下面是一份经过优化的动态规划方法。
它的核心思想是:不直接计算 K 个鸡蛋、N 层楼最少需要扔多少次,而是反过来计算,在 K 个鸡蛋、最多扔 m 次的情况下,最多能够确定多少层楼。
假设有:
K个鸡蛋。N层楼。- 存在一个临界楼层
F,从高于F的楼层扔下鸡蛋会碎,从F及以下扔不会碎。 - 鸡蛋碎了就不能继续使用,没有碎则可以重复使用。
目标是找到临界楼层 F,并使最坏情况下的投掷次数最少。
例如:
cpp
superEggDrop(2, 100); // 14
表示使用 2 个鸡蛋,在 100 层楼中确定临界楼层,最坏情况下至少需要 14 次投掷。
传统动态规划通常定义:
dpkn=k 个鸡蛋、n 层楼的最少投掷次数 dpkn=k\text{ 个鸡蛋、}n\text{ 层楼的最少投掷次数} dpkn=k 个鸡蛋、n 层楼的最少投掷次数
我们反过来定义:
dpmk=m 次投掷、k 个鸡蛋最多能确定的楼层数 dpmk=m\text{ 次投掷、}k\text{ 个鸡蛋最多能确定的楼层数} dpmk=m 次投掷、k 个鸡蛋最多能确定的楼层数
下面的
dp[k]是对投掷次数维度进行空间压缩后的结果。
核心状态转移方程
dpmk=dpm−1k−1+dpm−1k+1 dpmk=dpm-1k-1+dpm-1k+1 dpmk=dpm−1k−1+dpm−1k+1
假设我们现在从某一层扔下一枚鸡蛋,会有两种结果:

cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// K 个鸡蛋,N 层楼,最少需要扔多少次
int superEggDrop(int K, int N) {
if (N == 0 || K == 0) return 0;
// dp[k]:当前投掷次数下,k 个鸡蛋
// 最多能确定多少层楼
vector<long long> dp(K + 1, 0);
int moves = 0;
while (dp[K] < N) {
++moves;
// 当前是第 m 层, 通过第 m-1 层的状态来更新
// 因为需要用到上一层的状态,所以从大到小更新
// 保留上一轮的 dp[k-1]
for (int k = K; k >= 1; --k) {
dp[k] = min((long long)N,
dp[k] + dp[k - 1] + 1);
}
}
return moves;
}
int main() {
cout << superEggDrop(2, 100) << endl; // 14
cout << superEggDrop(2, 6) << endl; // 3
cout << superEggDrop(3, 14) << endl; // 4
}