返回首页

猜数字算法:从 Knuth 到 MCMC

本文分析了猜数字游戏的三种算法:Knuth 的 maximin(最优尝试次数)、每步 O(1) 复杂度的随机搜索,以及基于 Metropolis-Hastings 的 MCMC。提供 Python 代码、数学推导和 N=100 的实验结果。

猜数字:Knuth vs MCMC vs Random
Advertisement 728x90

优化数字猜谜策略:克努斯极小化极大法、随机搜索与马尔可夫链蒙特卡洛

在数字猜谜游戏中,玩家需通过提问和二元反馈(“更高”或“更低”)从区间 [1, N] 中找出一个隐藏整数。目标是在保证单步计算开销较低的前提下,最小化总猜测次数。本文对比分析三种主流策略:克努斯极小化极大法、随机搜索与马尔可夫链蒙特卡洛(MCMC)。

克努斯极小化极大法可严格保证理论最优解 ⌈log₂N⌉ 次猜测——但每步计算复杂度高达 O(N²)。随机搜索将单步复杂度降至 O(1),总猜测次数仅增加约 1.71 倍。MCMC 则在实践中取得精妙平衡:借助随机探索机制,高效收敛至与历史反馈一致的候选解集。

克努斯极小化极大策略

该算法始终维护一个可能值集合 S,并选择每次提问,以最大化 无论反馈是“更高”还是“更低” 都能确保实现的 |S| 缩减量。

Google AdInline article slot
  • 初始化 S = {1, ..., N};
  • 对每个候选猜测 a ∈ 所有可能隐藏值,计算 min(|{c ∈ S : check(a,c) = r}|),其中 r 取值于 {'>', '<'};
  • 选取上述最小值最大的 a 作为本次猜测;
  • 根据实际反馈更新 S。

该实现达到理论最优性能:当 N = 1024 时,恰好 10 步即可锁定答案。

from copy import deepcopy

def knut_maxmin(check, all_hiddens, results, cur_hiddens):
    """克努斯极小化极大策略:选取最优下一步猜测。"""
    return max(
        [(a, min(
            [sum(1 for c in cur_hiddens if check(a, c) != r)
             for r in results]
        )) for a in all_hiddens],
        key=lambda p: p[1]
    )[0]

def play_game(N):
    hiddens = list(range(1, N + 1))
    results = ['>', '<']

    def check(guess, hidden):
        if guess == hidden:
            return True
        return '>' if hidden > guess else '<'

    cur = list(hiddens)
    step = 0
    while len(cur) > 1:
        step += 1
        guess = knut_maxmin(check, cur, results, cur)
        answer = check(guess, SECRET)  # SECRET 为隐藏数字
        if answer is True:
            return guess, step
        cur = [c for c in cur if check(guess, c) == answer]
    return cur[0], step + 1

局限性:单步 O(N²) 复杂度使其在 N > 10⁵ 时难以实用。

基于区间的随机搜索

该方法不显式维护完整候选集 S,仅追踪当前有效区间 [lo, hi]。每次猜测从该区间内均匀随机采样,边界随反馈自适应更新。

Google AdInline article slot
import random

def play_random(N, secret):
    lo, hi = 1, N
    steps = 0
    while lo < hi:
        steps += 1
        guess = random.randint(lo, hi)
        if guess == secret:
            return steps
        elif secret > guess:
            lo = guess + 1
        else:
            hi = guess - 1
    return steps + 1

分析:若 x ~ Uniform[0,L],剩余区间长度的期望相对比例为 ∫₀¹ (x² + (1−x)²) dx = 2/3。因此期望猜测次数 k ≈ (ln N) / ln(3/2) ≈ 1.71 log₂N。当 N = 1024 时,平均约 17 步(对比极小化极大法的 10 步),但单步耗时恒定。

优势:

  • 单步时间复杂度为常数级;
  • 总猜测次数渐近为 O(log N);
  • 无需额外存储候选集合,内存开销极低。

基于 Metropolis–Hastings 的 MCMC 方法

该方法构建一条马尔可夫链,使其平稳分布 π(x) ∝ exp(−β · violations(x)),其中 violations(x) 表示 x 与历史查询–反馈对 [(guessᵢ, rᵢ)] 的冲突次数。

Google AdInline article slot

关键调参参数:σ(高斯提议噪声尺度)、β(逆温度系数)及 n_iter(MCMC 迭代步数)。

import random
import math

def mcmc_guess(N, history, n_iter=500, sigma=None, beta=10.0):
    """基于 Metropolis–Hastings 的数字猜谜算法。"""
    if sigma is None:
        sigma = N / 4

    def violations(x):
        count = 0
        for guess, response in history:
            if response == '>' and x <= guess:
                count += 1
            elif response == '<' and x >= guess:
                count += 1
        return count

    def target(x):
        return math.exp(-beta * violations(x))

    # 初始状态
    x = random.randint(1, N)

    for _ in range(n_iter):
        # 提议新状态
        x_prime = x + int(random.gauss(0, sigma))
        x_prime = max(1, min(N, x_prime))

        # 接受或拒绝
        acceptance = target(x_prime) / max(target(x), 1e-300)
        if random.random() < min(1.0, acceptance):
            x = x_prime

    return x


def play_mcmc(N, secret):
    history = []
    for step in range(1, 10 * N):
        guess = mcmc_guess(N, history)
        if guess == secret:
            return step
        response = '>' if secret > guess else '<'
        history.append((guess, response))
    return None  # 猜测失败

实验结果(N = 100,重复 100 次):均值 8.3 步,中位数 8 步,最大值 17 步。MCMC 能自然适配历史查询序列,在 σ = N/4、β = 10 的设定下实证表现稳健。

核心结论

  • 克努斯极小化极大法:理论最优 ⌈log₂N⌉ 步,但单步 O(N²);适用于 N 较小场景。
  • 随机搜索:猜测次数约为最优解的 1.71 倍,单步计算恒为 O(1);大规模 N 下首选方案。
  • MCMC 方法:经合理调参可达 ~log N 步效果;n_iter 步内即可收敛至可行解区域。
  • 策略选择取决于 N 的规模及每步实时响应约束。
  • 三者在前几步后均天然维持区间结构特征。

— Editorial Team

Advertisement 728x90

继续阅读