Powrót do strony głównej

Algorytmy wyszukiwania liczby: od Knutha do MCMC

Artykuł analizuje trzy algorytmy dla gry zgadywania liczby: maksymin Knutha z optymalną liczbą prób, losowe wyszukiwanie z O(1) złożonością na krok i MCMC na podstawie Metropolis-Hastingsa. Podano kod w Pythonie, matematyczny dowód i wyniki eksperymentalne dla N=100.

Zgadywanie liczby: Knuth vs MCMC vs Random
Advertisement 728x90

Optymalizacja strategii zgadywania liczby w grze logicznej

W grze zgadywania liczby gracz musi odgadnąć ukrytą wartość z zakresu [1, N], zadając pytania i otrzymując odpowiedzi typu „większe” lub „mniejsze”. Celem jest minimalizacja liczby prób przy jednoczesnym ograniczeniu obciążenia obliczeniowego na każdym kroku. Przedstawiamy trzy podejścia: maksymin Knutha, losowy wybór i metodę MCMC.

Maksymin zapewnia teoretyczny optimum w ⌈log₂N⌉ prób, ale wymaga O(N²) operacji na ruch. Losowy wybór redukuje złożoność do O(1), zwiększając średnią liczbę prób o 71%. MCMC oferuje kompromis między szybkością a dokładnością dzięki stochastycznemu przeszukiwaniu przestrzeni.

Strategia maksyminu Knutha

Algorytm maksyminu buduje zbiór S możliwych wartości i wybiera pytanie, które maksymalizuje gwarantowane zmniejszenie rozmiaru S niezależnie od odpowiedzi.

Google AdInline article slot
  • Zainicjuj S = {1, ..., N}.
  • Dla każdego a ∈ all_hiddens oblicz min(|{c ∈ S | check(a,c) = r}| dla r ∈ {'>', '<'}).
  • Wybierz a z maksymalną wartością min.
  • Zaktualizuj S na podstawie odpowiedzi.

Implementacja potwierdza optymalność: dla N = 1024 wystarczy 10 prób.

from copy import deepcopy

def knut_maxmin(check, all_hiddens, results, cur_hiddens):
    """Strategia maksyminu Knutha do wyboru optymalnego ruchu."""
    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 — ukryta liczba
        if answer is True:
            return guess, step
        cur = [c for c in cur if check(guess, c) == answer]
    return cur[0], step + 1

Ograniczenie: złożoność O(N²) na ruch czyni metodę niewykonalną dla N > 10⁵.

Losowe zgadywanie w przedziale

Podejście opiera się na binarnym przedziale [lo, hi] zamiast pełnego zbioru S. Każde pytanie to losowa liczba całkowita z zakresu randint(lo, hi), a granice są aktualizowane po każdej odpowiedzi.

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

Analiza: przy wyborze x ~ Uniform[0,L] średnia względna długość pozostałego przedziału wynosi ∫₀¹ (x² + (1−x)²) dx = 2/3. Liczba prób k ≈ (ln N) / ln(3/2) ≈ 1,71 log₂N. Dla N = 1024 to ok. 17 prób przeciwko 10 w przypadku optymalnym — ale z złożonością O(1) na ruch.

Zalety:

  • Stała złożoność obliczeniowa.
  • Asymptotycznie O(log N) prób.
  • Brak konieczności przechowywania całego zbioru S.

MCMC z algorytmem Metropolisa–Hastingsa

Metoda modeluje łańcuch Markowa zbieżny do rozkładu π(x) ∝ exp(−β · violations(x)), gdzie violations(x) to liczba sprzeczności z historią prób [(guess_i, r_i)].

Google AdInline article slot

Parametry: σ dla szumu gaussowskiego, β jako „temperatura”, n_iter iteracji.

import random
import math

def mcmc_guess(N, history, n_iter=500, sigma=None, beta=10.0):
    """Algorytm Metropolisa–Hastingsa do zgadywania liczby."""
    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))

    # Początkowy stan
    x = random.randint(1, N)

    for _ in range(n_iter):
        # Propozycja kandydata
        x_prime = x + int(random.gauss(0, sigma))
        x_prime = max(1, min(N, x_prime))

        # Akceptacja lub odrzucenie
        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  # nie odgadnięto

Eksperymenty (N = 100, 100 przebiegów): średnio 8,3 próby, mediana 8, maksimum 17. MCMC adaptuje się do historii — zalecane hiperparametry: σ = N/4, β = 10.

Kluczowe wnioski

  • Maksymin Knutha: teoretyczne optimum ⌈log₂N⌉ prób, ale O(N²) na ruch — tylko dla małych N.
  • Losowe zgadywanie: 1,71× więcej prób niż optimum, ale stała złożoność O(1) — idealne dla dużych N.
  • MCMC: około log N prób przy dobrej kalibracji; zbiega do zgodnych wartości po n_iter krokach.
  • Wybór strategii zależy od wielkości N oraz ograniczeń czasowych na pojedynczy ruch.
  • Wszystkie strategie zachowują strukturę przedziałową już po kilku pierwszych krokach.

— Editorial Team

Advertisement 728x90

Czytaj dalej