3 根绳子烧出 7 分钟
题
3 根不均匀的绳子,每根完整烧完恰好 8 分钟——但因为粗细不均,烧一半长度不等于烧 4 分钟。只有火柴,没有表。如何精确计时 7 分钟?
一句话
两端同时点燃能把一根绳的剩余燃烧时间精确减半;三根绳级联三次减半,\(4 + 2 + 1 = 7\),恰好是 \(8 \times \left(\frac{1}{2} + \frac{1}{4} + \frac{1}{8}\right)\)。
关键技巧
绳子不均匀意味着长度信息全部作废:不能对折找中点,不能按刻度剪。可靠的只剩两样——"某根绳烧完"这个事件,和"在事件发生的瞬间点火"这个操作。整道题就是用这两样原料搭一条事件链。
核心引理:无论多不均匀,两端同时点燃的绳,烧完耗时 = 剩余燃烧时长的一半。 证明只需换一个坐标系:不按长度、按"燃烧时间"给绳子标刻度(第 \(s\) 分钟烧到哪就标 \(s\))。每个火头每分钟恰好消耗 1 分钟的刻度,两个火头合计每分钟消耗 2 分钟,剩余量 \(R\) 自然在 \(R/2\) 后烧尽——两火头相遇的位置不确定,相遇的时刻却分毫不差。
于是"在另一端补一把火"就是一个精确操作:把这根绳当前的剩余时间除以 2。想测 \(7 = 8 - 1\),就要让最后一根绳恰好剩 1 分钟再补火,倒推出二进制展开 \(\frac{7}{8} = 0.111_2\)——每一位 1 对应一次减半。
解
\(t=0\) 时三根绳一起点,但点法不同:
- \(t=0\):绳 A 点两端,绳 B、绳 C 各点一端。
- \(t=4\):绳 A 烧完(\(8/2 = 4\))。此刻点燃绳 B 的另一端——B 已单端烧了 4 分钟,剩 4 分钟量,减半后还需 2 分钟。
- \(t=6\):绳 B 烧完。此刻点燃绳 C 的另一端——C 已单端烧了 6 分钟,剩 2 分钟量,减半后还需 1 分钟。
- \(t=7\):绳 C 烧完,计时结束。
{
"height": 260,
"grid": {"left": 60, "right": 40, "top": 40, "bottom": 30},
"legend": {"top": 0},
"xAxis": {"type": "value", "max": 8, "interval": 1, "name": "分钟"},
"yAxis": {"type": "category", "data": ["绳 C", "绳 B", "绳 A"]},
"series": [
{
"name": "单端燃烧", "type": "bar", "stack": "t", "barWidth": 22,
"itemStyle": {"color": "#f28e2c"}, "data": [6, 4, 0]
},
{
"name": "双端燃烧", "type": "bar", "stack": "t", "barWidth": 22,
"itemStyle": {"color": "#4e79a7"}, "data": [1, 2, 4],
"markLine": {
"symbol": "none",
"label": {"formatter": "7 分钟", "position": "insideEndTop"},
"lineStyle": {"color": "#e15759", "width": 2},
"data": [{"xAxis": 7}]
}
}
]
}
每根绳烧完的瞬间就是给下一根补火的信号,误差不会累积——每一步都由"烧完"这个物理事件对齐,而不是靠人估时间。
延伸:除了 7,还能计哪些时间
从最简单的往上爬:
- 一根绳:单端点燃得 8,双端点燃得 4。就这两个。
- 两根绳:级联一次得 6 \(= 4 + 2\);也可以往上接力——A 双端烧完再单端点 B 得 12 \(= 4 + 8\),纯接力得 16 \(= 8 + 8\)。面试经典原版"两根 60 分钟绳测 45 分钟"就是级联的 \(60 \times \frac{3}{4}\)。级联通式:\(n\) 根 \(T\) 分钟的绳可测 \(T\left(1 - 2^{-n}\right)\),本题的 7 就是 \(8 \times \frac{7}{8}\)。
- 三根绳:除了 7,还有两个不那么显然的——
- 9 分钟 \(= 4 + 2 + 3\):A、B 照原方案烧出事件 4 和 6,但 C 的单端改在 \(t=4\) 才点、\(t=6\) 补火。补火时 C 只烧了 2 分钟,剩 6 减半得 3,\(t=9\) 烧完。
- 10 分钟 \(= 4 + 4 + 2\):A 双端给出事件 4,B 纯单端给出事件 8;C 在 \(t=4\) 点单端、\(t=8\) 补火,剩 4 减半得 2,\(t=10\) 烧完。
- 完整清单:只允许在事件时刻点绳头的话,三根绳从点火起能测的恰好是 4、6、7、8、9、10、12、14、16、20、24 分钟(穷举见下方折叠块)。注意 8 分钟以内只有 4、6、7、8——想要 5 分钟,从划火柴那刻起是测不出来的。
- 允许计时起点延迟呢:如果只要求"从事件 X 到事件 Y 恰好 \(k\) 分钟",1 到 8 的每个整数都能测。比如 5 分钟就藏在 9 分钟方案里:从 \(t=4\)(A 烧完)到 \(t=9\)(C 烧完)。1 分钟则是原方案里 \(t=6\) 到 \(t=7\) 的那一段。
- 一般化到 \(n\) 根 \(T\) 时间的绳:可测时刻数 \(f(n) = 2, 5, 11, 23, 48, 101, 218, 473, \ldots\),即 OEIS A283075(出自 FiveThirtyEight 的 Riddler 栏目),没有已知闭式,只能穷举,经验增长率约 \(2.19^n\)。前 4 项恰好等于 \(3 \cdot 2^{n-1} - 1\),很容易让人以为找到了规律——但 \(n=5\) 时公式给 47,实际是 48,简单倍增的直觉在这里破裂。不过两条结构性规律不随 \(n\) 变复杂:\([0, T]\) 内可测的恰好只有级联家族 \(T\left(1-2^{-k}\right)\)(\(k = 0, \ldots, n\),共 \(n+1\) 个);最大可测 \(nT\)(纯接力),且一切可测值都是 \(T\) 的二进分数、分母至多 \(2^n\)。
- 为什么不能对折:对折找的是长度中点,不均匀时长度中点 \(\ne\) 时间中点。两端点燃有效,靠的是燃烧时间的守恒(两火头合计消耗速率恒定),与几何位置无关。
- 理论上限:每个新事件时刻都由"减半"和"相加"生成,所以一切可测时长都是 \(T\) 的二进分数(\(k/2^m\) 倍)——\(8/3\) 分钟这样的目标加多少根绳都测不出来。
- 允许点绳子中间呢:在任意内点点火会产生两个新火头,相当于把绳拆成两段"双端燃烧"。若某段烧完就立刻在剩余部分再点一个内点,始终维持 4 个火头,一根绳就能烧出 \(8/4 = 2\) 分钟——代价是要求"瞬间补火"这种理想化操作,通常只当趣味延伸。
穷举可测时刻
规则形式化:绳头只能在"事件时刻"(\(t=0\) 或某根绳烧完)点燃。一根绳若 \(t_1\) 先点一端、\(t_2\) 补另一端,烧完时刻为 \(\frac{t_1 + t_2 + 8}{2}\);永不补火则为 \(t_1 + 8\)。递归穷举所有策略:
from fractions import Fraction as F
T, N = F(8), 3
from_zero, intervals, seen = set(), set(), set()
def rec(events, ropes_left):
if (events, ropes_left) in seen: # 记忆化,N>=5 时必需
return
seen.add((events, ropes_left))
from_zero.update(events)
intervals.update(b - a for a in events for b in events if b > a)
if ropes_left == 0:
return
for t1 in sorted(events):
rec(events | {t1 + T}, ropes_left - 1) # 只点一端
for t2 in sorted(events):
if t1 <= t2 < t1 + T: # 在事件 t2 补火
rec(events | {(t1 + t2 + T) / 2}, ropes_left - 1)
rec(frozenset({F(0)}), N)
print(sorted(x for x in from_zero if x > 0))
# [4, 6, 7, 8, 9, 10, 12, 14, 16, 20, 24]
print(sorted(x for x in intervals if x <= 8))
# [1, 2, 3, 4, 5, 6, 7, 8]
把 N 换成一般值即可复现 OEIS A283075:
| \(n\) | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| 可测时刻数 \(f(n)\) | 2 | 5 | 11 | 23 | 48 | 101 | 218 | 473 |
前 6 项是本机穷举验证过的,7、8 两项取自 OEIS。OEIS 的评注还指出:如果允许"提前烧几根绳再开始计时"(即计时起点延迟,对应上文的区间玩法),可测集合会进一步变大。
数值验证
把绳子建模成一串随机长短的小段(每段有自己的燃烧秒数,总和 8 分钟),模拟火头逐段推进,验证无论怎么不均匀,三个事件时刻恒为 4、6、7:
import random
def both_ends_time(segs):
"""两端同时点燃,返回烧完耗时。segs 是每小段的燃烧时长。"""
segs, i, j, t = segs[:], 0, len(segs) - 1, 0.0
while i < j:
d = min(segs[i], segs[j])
t += d
segs[i] -= d; segs[j] -= d
if segs[i] == 0: i += 1
if segs[j] == 0: j -= 1
return t + segs[i] / 2 if i == j else t
def after_one_end(segs, t):
"""单端烧 t 分钟后剩下的段。"""
out = segs[:]
while t > 0 and out:
d = min(out[0], t)
out[0] -= d; t -= d
if out[0] == 0: out.pop(0)
return out
def rand_rope(n=12, total=8):
w = [random.random() for _ in range(n)]
return [x / sum(w) * total for x in w]
for _ in range(5):
A, B, C = rand_rope(), rand_rope(), rand_rope()
tA = both_ends_time(A)
tB = tA + both_ends_time(after_one_end(B, tA))
tC = tB + both_ends_time(after_one_end(C, tB))
print(round(tA, 9), round(tB, 9), round(tC, 9)) # 恒为 4.0 6.0 7.0