跳转至

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\) 时三根绳一起点,但点法不同:

  1. \(t=0\):绳 A 点两端,绳 B、绳 C 各点一端
  2. \(t=4\):绳 A 烧完(\(8/2 = 4\))。此刻点燃绳 B 的另一端——B 已单端烧了 4 分钟,剩 4 分钟量,减半后还需 2 分钟。
  3. \(t=6\):绳 B 烧完。此刻点燃绳 C 的另一端——C 已单端烧了 6 分钟,剩 2 分钟量,减半后还需 1 分钟。
  4. \(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