一、面试官问的是答案,还是假设?
智力题在面试里的地位一直很尴尬:候选人觉得它在考脑筋急转弯,面试官说它在考思维方式,双方都不太信对方的说法。更尴尬的是,很多流传甚广的「标准答案」本身是残缺的——它记住了一个结论,却把结论依赖的条件丢了。
下面三道题,分别落在算法、概率和几何里。它们都有一个广为流传的答案,也都至少漏掉了一个关键变量。
| 题目 | 流行答案 | 漏掉的变量 |
|---|---|---|
| 100 层楼、2 个鸡蛋 | 7 次(二分) | 鸡蛋数量;碎掉之后的线性成本 |
| 电梯平均 30 秒一班 | 平均等 15 秒 | 到达间隔的分布,不只是均值 |
| 井盖为什么是圆的 | 圆盖子掉不下去 | 「等宽」不等于「圆」;井沿还有一圈托边 |
把这三个变量补回去,三道题会从「背答案」变成「建模型」。
二、鸡蛋掉落:7 是无限个鸡蛋的答案
先复述题面:100 层楼,2 个一模一样的鸡蛋。鸡蛋从某一层落下,若没碎,可以继续用;若碎了就不能再用。要找出「刚好会碎」的临界楼层,问最坏情况下最少要扔多少次。
题面里有几个容易被含糊过去的地方,先钉死:
- 鸡蛋在 0 层一定不碎,在 100 层一定碎,否则问题无解;
- 要找的是精确的临界楼层,不是「证明某一层会碎」;
- 每次扔鸡蛋只有碎和不碎两种结果;
- 代价按最坏情况计,不按平均情况计。
如果只说「二分」,第一颗鸡蛋从 50 层扔:碎了,剩下 1 颗鸡蛋和 49 个候选楼层,只能从 1 层往上线性扫,最坏再扫 49 次,总共 50 次。没碎当然更好,但最坏情况是 50 次。
7 次是从哪来的?7 是信息论下界:2 的 7 次方是 128,大于 100,每次两种结果,7 次最多区分 128 种情况。这个下界只有在鸡蛋足够多、每次都能二分时才达得到。2 个鸡蛋时,碎掉的那一支付不起继续二分的成本。
正确做法是让第一颗鸡蛋的步长递减。第一次在 14 层扔:
- 碎了,用第二颗从 1 层线性扫到 13 层,最坏 1 + 13 = 14 次;
- 没碎,第二次在 14 + 13 = 27 层扔;碎了就从 15 层扫到 26 层,最坏 2 + 12 = 14 次;
- 还没碎,第三次在 27 + 12 = 39 层扔……
步长是 14、13、12、……、1,总和 105,覆盖 100 层绰绰有余。每一步都在给「碎掉的那一支」留余量:每扔一次没碎,就用掉了一次预算,同时碎掉之后还要线性扫的楼层上限也必须跟着减一。
把这件事写成递推,会得到一个比题面更好用的形式:
// dp[e][t]:e 个鸡蛋、t 次机会,最多能保证测出多少层function minDrops(floors: number, eggs: number): number { const maxT = floors; // 1 个鸡蛋时,最坏要扔 floors 次 const dp = Array.from({ length: eggs + 1 }, () => new Array(maxT + 1).fill(0), );
for (let t = 1; t <= maxT; t++) { for (let e = 1; e <= eggs; e++) { // 这一次扔下去: // 碎了 -> 下面还有 e-1 个鸡蛋、t-1 次机会 // 没碎 -> 上面还有 e 个鸡蛋、t-1 次机会 // 再加上当前这一层 dp[e][t] = dp[e - 1][t - 1] + 1 + dp[e][t - 1]; } if (dp[eggs][t] >= floors) return t; } return floors;}这个递推和 LeetCode 887 是同一个东西,只是换了个方向:不是问「n 层楼最少扔几次」,而是问「t 次机会最多能覆盖几层」。后者少一层嵌套循环,写起来短很多。
把递推展开,得到闭式:
F(t, e) = C(t, 1) + C(t, 2) + ... + C(t, e)鸡蛋数为 2 时,它是三角数 t(t+1)/2:t = 13 覆盖 91 层,t = 14 覆盖 105 层,所以 2 个鸡蛋的答案是 14。鸡蛋更多时,100 层对应的最少次数是:
| 鸡蛋数 | 100 层最少次数 |
|---|---|
| 1 | 100 |
| 2 | 14 |
| 3 | 9 |
| 4 | 8 |
| 5 及以上 | 7 |
5 个鸡蛋以上就撞到信息论下界了:7 次两分叉最多区分 128 种情况,100 层已经够用。再多的鸡蛋不会让答案小于 7,只会让「碎了也不心疼」这件事变得更有底气。
面试现场常见的追问是:「如果要求平均次数最少呢?」那是另一个目标函数,答案会变。先问清楚是最坏情况还是期望,再动手。7 和 14 都不算错,错的是不说明鸡蛋数量。
三、电梯:平均等待不是间隔的一半
电梯题通常有两个版本:一个是「设计调度算法」,一个是「为什么等电梯总觉得久」。前者考系统设计,后者考概率。它们共享一个容易被忽略的前提:电梯到达的间隔不是一个固定值。
先看等待。假设电梯平均 30 秒来一班。直觉说平均等 15 秒,因为「最坏等 30 秒,理想情况 0 秒」。这个直觉只在电梯准点到站时成立:如果每 30 秒准时来一班,你的到达时刻在 30 秒里均匀分布,平均等待确实是 15 秒。
但如果到达间隔是随机的,结论会变。对一个随机到达的乘客,平均等待时间是:
E[W] = E[T²] / (2 · E[T])其中 T 是相邻两班电梯的间隔。分母是一次间隔的均值,分子是间隔平方的均值。平方会把长间隔放大,所以乘客更容易撞上长间隔——这就是 inspection paradox(检查悖论)。用更直白的话说:你不是在「随机挑一班电梯」,你是在「按时间随机落点」,落点越长的那段间隔,越容易被你踩中。
对泊松过程,间隔服从指数分布,E[T] = 1/λ,E[T²] = 2/λ²,代进去 E[W] = 1/λ,也就是平均间隔本身,而不是一半。30 秒一班,平均等 30 秒;准点 30 秒一班,平均等 15 秒。均值一样,等待差一倍。
| 到达间隔 | E[T] | E[W] |
|---|---|---|
| 固定 30 秒 | 30 秒 | 15 秒 |
| 泊松,均值 30 秒 | 30 秒 | 30 秒 |
| 方差比泊松更大 | 30 秒 | 大于 30 秒 |
这也解释了为什么「公交车要么不来,要么一起来」:车不是变少了,是间隔的方差变大了;而你在站台等车时,恰好更容易落进那段长间隔。
写几行代码就能看到差别。下面用两种方式生成电梯到达,再让乘客在时间轴上随机出现,量一量等到下一班的时间:
function nextArrivalWait(kind: "fixed" | "poisson", mean = 30) { const n = 200_000; const horizon = n * mean * 2; const arrivals: number[] = []; let t = 0;
while (t < horizon) { t += kind === "fixed" ? mean : -mean * Math.log(1 - Math.random()); arrivals.push(t); } arrivals.push(t + mean); // 兜底,保证最后一班之后也有下一班
let total = 0; for (let i = 0; i < n; i++) { const passenger = Math.random() * horizon; let lo = 0; let hi = arrivals.length; while (lo < hi) { const mid = (lo + hi) >> 1; if (arrivals[mid] < passenger) lo = mid + 1; else hi = mid; } total += arrivals[lo] - passenger; } return total / n;}
console.log(nextArrivalWait("fixed")); // ≈ 15console.log(nextArrivalWait("poisson")); // ≈ 30这段代码只模拟了「等到下一班」的时间,没有模拟电梯容量、方向和中途停靠。它想说明的也只有一件事:只看平均间隔,算不出平均等待;还得看间隔的分布。
如果题目换成「设计电梯调度」,那就是另一个模型。先把目标函数问出来:是最小化平均等待,还是最小化 p99 等待,还是最大化吞吐、降低能耗?目标不同,算法不同。
| 策略 | 规则 | 优点 | 失效场景 |
|---|---|---|---|
| FCFS | 按请求顺序服务 | 公平、实现简单 | 满员请求可能拖住整条队列 |
| SSTF | 先服务最近的请求 | 平均等待短 | 边角楼层可能长期等不到 |
| SCAN | 沿一个方向扫到尽头再反向 | 吞吐稳定、行为可预测 | 空载时也会跑到楼层尽头 |
| LOOK | 到最后一个请求就反向 | 比 SCAN 少跑空程 | 请求稀疏时方向切换频繁 |
| 目的楼层调度 | 按目的地分组 | 减少停靠次数 | 多一套输入终端和故障路径 |
经典答案是 SCAN:电梯沿一个方向一路服务,到顶或到底再反向,像磁盘磁头一样。LOOK 是它的实用版:到最后一个请求就反向,不必跑到楼层尽头。它们的共同优点是「可预测」:乘客知道电梯大概会往哪个方向走,不会因为远处来了一个请求就反复掉头。FCFS 公平,但容易被一个满员请求拖住;SSTF 平均等待短,但边角楼层可能一直等不到。
真实楼宇里,调度还要考虑早高峰的上行潮汐、午休的双向流量、消防和疏散模式。目的楼层调度让乘客在进电梯前先输入楼层,系统把去同一层的人分到一组,减少停靠次数;代价是多了一套输入终端和一条新的故障路径。面试里说「用 SCAN」不算错,但只说了算法,没说目标函数和失效场景。
至于关门键,按下去有没有用,取决于电梯的配置;在某些楼里,它更像一个安慰按钮。真正让门关上的,通常是你后退半步,而不是连按三下。
四、井盖:圆不是仅有的解,只是省事的解
标准答案通常是:圆盖子不会掉进圆洞里,因为圆的宽度在每个方向都一样,都是直径;洞的直径比盖子略小,所以无论怎么转、怎么斜,盖子都卡在洞口。方形盖子就不行:斜着放、立起来,总能找到一个角度掉下去。
这个答案对了一半。对的部分是「等宽」:圆在任意方向上的宽度都相同,而且大于洞的直径,所以无论怎么转、怎么斜,投影都塞不进洞里。错的部分是把「等宽」直接等同于「圆」。等宽曲线不止圆一种。
最常被拿来翻案的是 Reuleaux 三角形:以等边三角形的三个顶点为圆心、边长为半径画三段圆弧,围成的图形宽度处处相等。英国 50 便士硬币的七边形轮廓也是等宽曲线。用 Reuleaux 三角形做井盖,只要它的宽度大于洞的直径,同样掉不下去。所以「井盖为什么是圆的」的严格答案不是「只有圆不会掉」,而是「等宽形状不会掉,而圆是其中更容易制造、更容易滚动、也更不需要对准的一种」。
为什么等宽就够了?把盖子想成一张薄板。无论你怎么倾斜它,它在洞口平面上的投影,在平面内任意方向上的宽度,都等于盖子在该方向上的宽度;等宽形状的每个方向都是同一个宽度 w。一个直径小于 w 的圆洞,装不下任何一个方向宽度都为 w 的图形。方形则不同:它的宽度随方向变化,从边长 s 到对角线 s√2;沿着一条边把盖子倾斜到接近竖直,投影会收成一条长约 s 的细长条。只要 s 小于洞的对角线 a√2(常见的盖子和洞口只差一点余量,这个条件成立),这条投影就能斜着穿过洞口。
| 形状 | 等宽 | 能掉进匹配的洞 | 现实中的取舍 |
|---|---|---|---|
| 圆形 | 是 | 不能 | 好滚、不用对准、好加工 |
| 正方形 | 否 | 能(斜着或立起来) | 需要对准,角容易磕 |
| Reuleaux 三角形 | 是 | 不能 | 不好滚,加工麻烦 |
| 带托边的任意形状 | — | 通常不能 | 托边才是第一道保险 |
不过,现实中的井盖还有一圈托边:盖子边缘搭在井圈的凸缘上,就算形状允许,它也不会真的掉下去。很多方形、矩形的检修盖板也活得好好的。所以「圆所以掉不下去」只是把几何条件当成了全部原因;施工、制造、搬运、防盗,都是答案的一部分。
面试里更好的回答不是背「圆不会掉」,而是先问:这个「为什么」问的是几何、制造,还是施工?
- 如果问几何,答案是等宽:圆和 Reuleaux 三角形都不会掉;
- 如果问制造,答案是圆更容易加工、更容易滚动、更容易存放;
- 如果问施工,答案是圆洞更容易挖,盖子不用对准。
三个问题,三个答案。最省事的回答是「因为洞是圆的」——这句话当然对,但它把问题原样退回了面试官。面试官如果继续问「那洞为什么是圆的」,你就从几何学滑进了施工队的地盘。
五、三道题的共同点:标准答案都被压缩过
把三道题放在一起,会发现它们的「标准答案」都遵循同一个压缩方式:保留结论,删掉目标函数和约束。
- 鸡蛋:删掉了「2 个鸡蛋」和「最坏情况」,于是 7 这个无限鸡蛋的下界被当成了答案;
- 电梯:删掉了「到达间隔的分布」,于是均值被当成了全部信息;
- 井盖:删掉了「等宽」和「托边」,于是圆被当成了仅有的解。
面试里真正值钱的动作,是在回答之前把这些被删掉的东西问回来:
- 目标是什么?最坏情况、平均情况,还是 p99?
- 约束是什么?鸡蛋能不能复用,电梯是不是准点,井盖有没有托边?
- 你要的是可行解、下界,还是实现成本低的解?
- 你的答案在什么条件下失效?
这四问不需要多聪明,只需要在开口前停两秒。智力题考的往往不是那两秒里的灵感,而是你愿不愿意承认:题目本身没说全。
当然,真实工作里的题面通常更直接:井盖要圆,预算要方,deadline 是昨天,而且没有标准答案——只有下一位面试官。
参考来源
- LeetCode 887. Super Egg Drop — https://leetcode.com/problems/super-egg-drop/
- Wikipedia: Inspection paradox — https://en.wikipedia.org/wiki/Inspection_paradox
- Wikipedia: Elevator algorithm — https://en.wikipedia.org/wiki/Elevator_algorithm
- Wikipedia: Manhole cover — https://en.wikipedia.org/wiki/Manhole_cover
- Wikipedia: Reuleaux triangle — https://en.wikipedia.org/wiki/Reuleaux_triangle
- Wikipedia: Curve of constant width — https://en.wikipedia.org/wiki/Curve_of_constant_width
- Wikipedia: Bus bunching — https://en.wikipedia.org/wiki/Bus_bunching