Выигрышные и проигрышные позиции

Игры с полной информацией и ключевая идея теории игр: разметка позиций на выигрышные и проигрышные.

Выигрышная позиция — та, из которой есть ход в проигрышную (для соперника); проигрышная — та, из которой все ходы ведут в выигрышные.

Зачем это в олимпиадах

Игровые задачи — отдельный любимый жанр олимпиад: «двое по очереди берут камни, кто не может ходить — проиграл, кто выигрывает при правильной игре». За кажущейся простотой стоит стройная теория. Базовый навык — научиться размечать все позиции на выигрышные (W) и проигрышные (L) и понимать, почему такая разметка определяет исход. Это фундамент для функции Шпрага–Гранди и нима в следующих уроках. Освоив разметку, вы будете решать целый класс задач, не угадывая, а доказывая.

Какие игры мы рассматриваем

Мы изучаем беспристрастные игры с полной информацией: оба игрока видят всю позицию (нет скрытых карт), нет случайности (нет кубиков), доступные ходы зависят только от позиции, а не от того, чей ход (беспристрастность), игра конечна и кто не может сделать ход — проигрывает (нормальное соглашение). Примеры: «отнимание камней», ним, многие задачи про передвижение фишек. Шахматы и крестики-нолики сюда не входят (там ходы зависят от цвета — игры пристрастные).

Идея разметки W/L «на пальцах»

Рассуждаем с конца. Терминальная позиция, где ходить некуда, — проигрышная для того, чей ход (он уже проиграл). Дальше по индукции: позиция выигрышная, если из неё есть хотя бы один ход в проигрышную (отдаём сопернику плохую позицию); позиция проигрышная, если все ходы ведут только в выигрышные (что ни сделай — отдашь сопернику победу). Эти два правила однозначно размечают все позиции снизу вверх.

# Игра: есть n камней, за ход берут 1, 2 или 3. Кто не может ходить — проиграл.
def compute_states(n, moves):
    win = [False] * (n + 1)        # win[k] = выигрышна ли позиция с k камнями
    for k in range(1, n + 1):
        # позиция выигрышна, если есть ход в проигрышную
        win[k] = any(k - m >= 0 and not win[k - m] for m in moves)
    return win

win = compute_states(12, (1, 2, 3))
for k in range(13):
    print(k, "W" if win[k] else "L")

Вывод:

0 L
1 W
2 W
3 W
4 L
5 W
6 W
7 W
8 L
9 W
10 W
11 W
12 L

Проигрышные позиции — кратные 4 (0, 4, 8, 12). Это не случайность: при ходах 1–3 первый игрок всегда может «дополнить» ход соперника до 4 и удерживать остаток кратным 4. Из любого 4k что ни возьми (1, 2 или 3), соперник возьмёт дополнение до 4. Так разметка вскрыла закономерность.

Почему разметка определяет исход

Утверждение: если позиция помечена W, то у текущего игрока есть стратегия победы; если L — проигрывает при любой игре (при оптимальной игре соперника). Доказательство — индукция по «глубине» игры. В W-позиции игрок ходит в L-позицию (она существует по определению W), и теперь уже соперник в проигрышной позиции — по предположению индукции он проиграет. В L-позиции любой ход ведёт в W-позицию, отдавая сопернику выигрышную стратегию. База — терминальная L-позиция, где текущий игрок уже проиграл. Индукция замыкается.

Разбор: игра с произвольным набором ходов

Разметка не требует угадывать формулу — она механически работает для любого множества разрешённых ходов. Возьмём ходы {2, 3} (брать ровно 2 или 3 камня) и посмотрим, где проигрышные позиции.

def compute_states(n, moves):
    win = [False] * (n + 1)
    for k in range(1, n + 1):
        win[k] = any(k - m >= 0 and not win[k - m] for m in moves)
    return win

win = compute_states(15, (2, 3))
losing = [k for k in range(16) if not win[k]]
print("Проигрышные позиции:", losing)

Вывод:

Проигрышные позиции: [0, 1, 5, 6, 10, 11, 15]

Закономерность сложнее (период 5: {0,1} по модулю 5), и угадать её сходу непросто — но разметка нашла её автоматически за O(n·|moves|). Это и ценно: когда формула неочевидна, всегда можно построить таблицу W/L перебором и либо использовать её напрямую, либо разглядеть период.

Разбор олимпиадной задачи: игра на двух кучах

Усложним: две кучи, за ход берут любое число камней из одной кучи или поровну из обеих. Кто не может ходить — проиграл. Это игра Витхоффа. Размечать вручную тяжело, но перебор справляется. Построим таблицу W/L для всех позиций (a, b) и поищем закономерность в проигрышных.

def wythoff(N):
    win = [[False] * (N + 1) for _ in range(N + 1)]
    for a in range(N + 1):
        for b in range(N + 1):
            # перебираем все ходы из (a, b)
            moves = []
            for k in range(1, a + 1):
                moves.append((a - k, b))          # из первой кучи
            for k in range(1, b + 1):
                moves.append((a, b - k))          # из второй кучи
            for k in range(1, min(a, b) + 1):
                moves.append((a - k, b - k))      # поровну из обеих
            win[a][b] = any(not win[na][nb] for na, nb in moves)
    return win

win = wythoff(12)
losing = [(a, b) for a in range(13) for b in range(a, 13) if not win[a][b]]
print(losing)

Вывод:

[(0, 0), (1, 2), (3, 5), (4, 7), (6, 10)]

Проигрышные позиции — (0,0), (1,2), (3,5), (4,7), (6,10), …. Это знаменитые пары Витхоффа, связанные с золотым сечением: aᴪ = ⌊k·φ⌋, bᴪ = ⌊k·φ2. Сходу такую формулу не угадать — но разметка перебором нашла позиции автоматически, и уже по таблице можно либо вывести закономерность, либо использовать её напрямую при малых ограничениях. В этом и сила метода: он не требует озарения, лишь аккуратной индукции по позициям.

Подводные камни

  • Соглашение об окончании. Мы используем «нормальное»: кто не может ходить — проиграл. В «мизерном» варианте (кто не может — выиграл) разметка другая. Читайте условие.
  • Беспристрастность. Теория W/L и Гранди — для игр, где ходы не зависят от игрока. Для шахмат и т.п. она неприменима.
  • Размер таблицы. Разметка занимает O(n) памяти; для огромных n ищите период или формулу.
  • Граница ходов. Не забывайте проверку k - m >= 0, иначе уйдёте в отрицательные индексы.

Итог

  • Изучаем беспристрастные игры с полной информацией без случайности.
  • Терминальная позиция (нет ходов) — проигрышная.
  • W: есть ход в L; L: все ходы ведут в W — это размечает все позиции по индукции.
  • Разметка перебором находит исход и часто вскрывает период проигрышных позиций.
Проверьте себя
1. Какая позиция считается выигрышной (W)?
AИз которой все ходы ведут в выигрышные
BИз которой есть хотя бы один ход в проигрышную позицию
CТерминальная позиция без ходов
DЛюбая позиция с чётным числом камней
2. Чем является терминальная позиция (ходить некуда) при нормальном соглашении?
AВыигрышной
BПроигрышной
CНейтральной
DЗависит от числа камней
3. В игре «взять 1, 2 или 3 камня» какие позиции проигрышны?
AЧётные
BКратные 3
CКратные 4
DПростые числа
4. Для каких игр применима теория разметки W/L?
AДля любых игр, включая шахматы
BДля игр со случайностью
CДля беспристрастных игр с полной информацией
DТолько для нима
Поддержать проект