Курс№19–21. Теория игр0%
Теория · номера 19, 20 и 21 из 27

Теория игр

Две кучи камней и двое игроков. Одна функция и три разных вызова дают ответы сразу на три номера экзамена.

11 разделов~60 минут3 первичных балла
  1. 1Что проверяют эти номера
  2. 2Общее решение: функция f(s, m)
  3. 3№19: Ваня выигрывает первым ходом
  4. 4№20: Петя выигрывает вторым ходом
  5. 5№21: Ваня выигрывает первым или вторым ходом
  6. 6Две кучи
  7. 7Игры «минус»
  8. 8Необычные игры
  9. 9Как решать
  10. 10Ловушки
  11. 11Домашка
Раздел 1 из 11

Что проверяют эти номера

№19, №20 и №21 это три вопроса про одну и ту же игру. Условие у них общее, функция общая, меняется только последняя строка. Поэтому учим их вместе: разобрался с одним, считай, разобрался со всеми.

номервопросвызов
№19Ваня выигрывает первым ходомf(s, 2)
№20Петя выигрывает вторым ходомnot f(s, 1) and f(s, 3)
№21Ваня выигрывает первым или вторым ходом, но не всегда первымnot f(s, 2) and f(s, 4)

Звучит как задача на логику, но перебирать ходы в голове не нужно. Компьютер проверяет все варианты сам, от нас требуется правильно записать правила игры и что значит «выигрывает». Коротко говоря: игрок, который ходит, выбирает лучший ход, а его соперник может выбрать любой.

Два игрока, Петя ходит первым. Ход меняет кучу (или кучи) камней. Игра кончается, когда набран порог. Выигрывает тот, кто сделал последний ход. Надо найти начальные значения S, при которых выполняется условие про выигрыш за один, два, три или четыре хода.

Простыми словами

В игре есть двое, и код всегда считает за одного игрока: за того, кто должен выиграть. Назовём его «наш». Вся логика укладывается в два правила:

Число m это сколько ходов осталось сделать обоим вместе. В №19 первым ходит Петя, а выиграть должен Ваня, поэтому m = 2: ход Пети, затем ход Вани.

Сразу запомни, как считать ходы: Петя, Ваня, Петя, Ваня. Первый ход Пети это 1, первый ход Вани это 2, второй ход Пети это 3, второй ход Вани это 4.

Раздел 2 из 11

Общее решение: функция f(s, m)

s это камни, а m это сколько ходов осталось. На ходу того, кто должен выиграть, хватает одного удачного варианта, поэтому any. На ходу соперника выигрыш должен быть при любом его ответе, поэтому all.

m — количество ходов (полуходов, то есть ходов обоих игроков), s — количество камней.

python
def f(s, m):
    if s >= 39:                       # игра закончилась
        return m % 2 == 0
    if m == 0:
        return 0
    h = [f(s + 1, m - 1), f(s + 3, m - 1), f(s * 2, m - 1)]    # ходы
    return any(h) if m % 2 != 0 else all(h)

print([s for s in range(1, 39) if f(s, 2)])                         # №19
print([s for s in range(1, 39) if not f(s, 1) and f(s, 3)])        # №20
print([s for s in range(1, 39) if not f(s, 2) and f(s, 4)])        # №21

Как это читать:

Разбор игры руками

Одна куча, ходы «+1» и «×2», конец при 30 камнях. Возьмём S = 14.

Петя может сделать 15 или 28. Из 15 Ваня удваивает и получает 30. Из 28 Ваня тоже удваивает и получает 56. Как бы ни сходил Петя, Ваня выигрывает первым ходом. Это и проверяет f(14, 2).

ситуацияв коде
ходит тот, кто должен выиграть, m нечётноехватает одного хода: any
ходит его соперник, m чётноегодятся все ходы: all
куча дошла до концапоследний ход должен быть за нужным игроком: m % 2 == 0
ходы кончилисьне успели: 0
Раздел 3 из 11

№19: Ваня выигрывает первым ходом

Три вызова внизу дают ответы сразу на все три номера. На экзамене код пишешь один раз и получаешь три ответа.

Ходы +2, +4, ×2, порог 125:

python
def f(s, m):
    if s >= 125:
        return m % 2 == 0
    if m == 0:
        return 0
    h = [f(s + 2, m - 1), f(s + 4, m - 1), f(s * 2, m - 1)]
    return any(h) if m % 2 != 0 else all(h)

print([s for s in range(1, 125) if f(s, 2)])                          # [61, 62]
print([s for s in range(1, 125) if not f(s, 1) and f(s, 3)])         # [31, 57, 58, 59, 60]
print([s for s in range(1, 125) if not f(s, 2) and f(s, 4)])         # [55, 56]
Попробуй самучебная задача

Одна куча, ходы «+1» и «×2», игра заканчивается при 40 камнях и более, начальное число S от 1 до 39. Найди наименьшее S, при котором Ваня выигрывает первым ходом при любом ходе Пети.

Подсказка

Вызов f(s, 2).

Раздел 4 из 11

№20: Петя выигрывает вторым ходом

Петя ходит первым. Его второй ход по общему счёту третий: Петя, Ваня, Петя. Поэтому нам нужно f(s, 3).

Но есть подвох: f(s, 3) считает и те случаи, где Петя мог выиграть сразу, первым ходом. В условии просят именно второй ход, поэтому такие начальные значения убираем условием not f(s, 1). Читается так: «первым ходом гарантированно не выиграть, а вторым можно».

Разбор руками

Та же игра с порогом 30. S = 7: Петя делает 14. Из 14 Ваня может получить 15 или 28, и оба раза Петя удваивает и заканчивает игру. Первым ходом Петя выиграть не мог: из 7 получается только 8 или 14. Поэтому S = 7 подходит.

Попробуй самучебная задача

Одна куча, ходы «+1» и «×2», конец при 30 и более, S от 1 до 29. Найди два наименьших S, при которых Петя выигрывает вторым ходом. Запиши через пробел по возрастанию.

Подсказка

not f(s, 1) and f(s, 3).

Попробуй самучебная задача

Одна куча, ходы «+2» и «×2», конец при 50 и более, S от 1 до 49. Найди наименьшее S, при котором Петя выигрывает вторым ходом.

Раздел 5 из 11

№21: Ваня выигрывает первым или вторым ходом

Теперь выиграть должен Ваня, первым или вторым своим ходом. Его ходы по общему счёту второй и четвёртый: Петя, Ваня, Петя, Ваня. Поэтому f(s, 4).

Условие not f(s, 2) убирает начальные значения, где Ваня всегда побеждал бы первым ходом. Смысл такой: Петя может сыграть так, что сразу выиграть не выйдет, но Ваня всё равно победит не позже второго хода.

Разбор руками

Порог 30. S = 12: Петя делает 13 или 24. Из 24 Ваня сразу удваивает и выигрывает. Из 13 он идёт в 14, а это уже проигрышная для Пети позиция, и Ваня выигрывает следующим ходом. Гарантировать победу первым ходом он не может, потому что из 13 сразу до 30 не дойти.

Попробуй самучебная задача

Одна куча, ходы «+1» и «×2», конец при 30 и более, S от 1 до 29. Найди наименьшее S, при котором Ваня выигрывает первым или вторым ходом при любой игре Пети, но не может гарантированно выиграть первым.

Подсказка

not f(s, 2) and f(s, 4).

Попробуй самучебная задача

Одна куча, ходы «+2» и «×2», конец при 50 и более, S от 1 до 49. Найди наименьшее S для условия №21.

Раздел 6 из 11

Две кучи

Функция та же, только теперь у неё два числа и по каждой команде два варианта хода, в первую кучу и во вторую. Условие конца обычно про сумму.

Функция получает две кучи a и b, порог на сумму:

python
def f(a, b, m):
    if a + b >= 90:
        return m % 2 == 0
    if m == 0:
        return 0
    h = [f(a + 3, b, m - 1), f(a, b + 3, m - 1), f(a * 2, b, m - 1), f(a, b * 2, m - 1)]
    return any(h) if m % 2 != 0 else all(h)

print([s for s in range(1, 78) if f(12, s, 2)])     # Ваня выигрывает при любом ходе Пети

Первая куча известна (12), вторая S — перебираем.

Для двух куч общая функция тоже одна, и она сразу отвечает на все три вопроса:

python
def f(a, b, m):
    if a + b >= 90:
        return m % 2 == 0
    if m == 0:
        return 0
    h = [f(a + 3, b, m - 1), f(a, b + 3, m - 1), f(a * 2, b, m - 1), f(a, b * 2, m - 1)]
    return any(h) if m % 2 != 0 else all(h)

print([s for s in range(1, 78) if f(12, s, 2)])                          # №19
print([s for s in range(1, 78) if not f(12, s, 1) and f(12, s, 3)])     # №20
print([s for s in range(1, 78) if not f(12, s, 2) and f(12, s, 4)])     # №21

Если условие просит два наименьших значения, бери первые два числа из списка.

Неудачный ход Пети

Если в условии «после неудачного хода Пети» (Петя ходит как угодно, лишь бы не закончил игру), в конце ставим any для обоих игроков:

python
    return any(h) if m % 2 != 0 else any(h)         # или просто return any(h)

Вызов остаётся тем же, меняется только последняя строка функции.

Дополнительное условие

Если у игры есть особая зона, дописываем её ПЕРЕД обычной проверкой конца:

python
def f(s, m):
    if 50 <= s <= 119:
        return m % 2 == 0
    if s > 119:
        return m % 2 != 0
    if m == 0:
        return 0
    h = [f(s + 1, m - 1), f(s + 3, m - 1), f(s * 2, m - 1)]
    return any(h) if m % 2 != 0 else all(h)
Попробуй самучебная задача

Одна куча, ходы «+1» и «×2», конец при 30 и более, S от 1 до 29. Ваня выигрывает первым ходом после неудачного хода Пети. Найди наименьшее S.

Подсказка

Замени all на any.

Попробуй самучебная задача

Две кучи, в первой 5 камней. В любую кучу можно добавить 1 камень или удвоить её, конец при сумме 40 и более. Найди наименьшее S, при котором Петя выигрывает вторым ходом.

Раздел 7 из 11

Игры «минус»

Здесь камни убирают, и игра кончается, когда их стало мало. Меняется условие конца и ходы, а вся логика и все три вызова остаются прежними. Сверху S не ограничена, поэтому диапазон берём с запасом и проверяем, что у его конца новых значений уже не появляется.

python
def f(a, b, m):
    if a + b <= 30:
        return m % 2 == 0
    if m == 0:
        return 0
    h = [f(a - 1, b, m - 1), f(a, b - 1, m - 1), f(a // 2, b, m - 1), f(a, b // 2, m - 1)]
    return any(h) if m % 2 != 0 else all(h)

print([s for s in range(21, 500) if f(10, s, 2)])                          # №19
r = [s for s in range(21, 500) if not f(10, s, 1) and f(10, s, 3)]         # №20
print(r[0], r[-1])
print(min(s for s in range(21, 500) if not f(10, s, 2) and f(10, s, 4)))   # №21
Попробуй самучебная задача

Одна куча, ходы «−1» и «поделить нацело на 2», игра заканчивается при 10 камнях и менее, S от 11 до 299. Найди наименьшее S для того же условия про Ваню.

Раздел 8 из 11

Необычные игры

Встречаются правила с запретом повторять ход. Такие игры решаются тем же кодом, если передавать в функцию прошлый ход.

Произведение камней вместо суммы:

python
def f(a, b, m):
    if a * b >= 516:
        return m % 2 == 0
    if m == 0:
        return 0
    h = [f(a + 3, b, m - 1), f(a, b + 3, m - 1)]
    return any(h) if m % 2 != 0 else all(h)

print([s for s in range(1, 74) if f(7, s, 2)])

Нельзя повторять последний ход (запоминаем его в hist):

python
def f(s, m, hist=''):
    if s >= 60:
        return m % 2 == 0
    if m == 0:
        return 0
    h = []
    if hist[-1:] != '1':
        h.append(f(s + 3, m - 1, hist + '1'))
    if hist[-1:] != '2':
        h.append(f(s + 6, m - 1, hist + '2'))
    return any(h) if m % 2 != 0 else all(h)

hist[-1:] (срез) вместо hist[-1], чтобы не падало на пустой строке.

Запоминаем последний и предпоследний ход:

python
def f(s, m, k='', k2=''):
    if s > 40:
        return m % 2 == 0
    if m == 0:
        return 0
    h = []
    if k2 != '+1':
        h.append(f(s + 1, m - 1, '+1', k))
    if k2 != '+2':
        h.append(f(s + 2, m - 1, '+2', k))
    if k2 != '*2':
        h.append(f(s * 2, m - 1, '*2', k))
    return any(h) if m % 2 != 0 else all(h)
Раздел 9 из 11

Как решать

Весь порядок действий на экзамене укладывается в несколько минут.

  1. Определи: одна куча или две, порог >= или <=.
  2. Перепиши ходы в список h.
  3. Для «минус» игр условие конца a + b <= T, ходы например a - 1, a // 2.
  4. Вызов по номеру: №19 f(…, 2) (при неудачном ходе Пети any в обеих ветках), №20 not f(…, 1) and f(…, 3), №21 not f(…, 2) and f(…, 4).
  5. Диапазон S с запасом (в играх «минус» берём до 1500).
Попробуй самучебная задача

Одна куча, ходы «+2» и «×2», конец при 50 камнях и более, S от 1 до 49. Найди наименьшее S, при котором Ваня выигрывает первым ходом при любом ходе Пети.

Раздел 10 из 11

Ловушки

Ловушка

Не перепутай any и all: any для того, кто выигрывает, all — когда ходит противник.

Ловушка

«Неудачный ход» — any в обеих ветках.

Ловушка

Игра закончилась раньше: ответ определяет чётность m (m % 2 == 0).

Ловушка

В «минус» играх S не ограничена сверху, в №20 просят наименьшее и наибольшее.

Ловушка

Не забывай m == 0: return 0 после проверки конца.

Ловушка

«Наименьшее и наибольшее» и «два наименьших» — разные вопросы (r[0], r[-1] и r[:2]).

Ловушка

not f(s, 1) — отдельное условие, не забывай его.

Ловушка

Диапазон S для игр «минус» с запасом.

Ловушка

Нужно именно not f(s, 2) и f(s, 4). f(s, 4) включает и выигрыш за два полухода, поэтому без not f(s, 2) ответ был бы меньше.

Раздел 11 из 11

Домашка

Задачи с настоящих экзаменов и из банка ФИПИ. Они идут тройками: сначала №19, потом №20 и №21 про ту же игру. Код пишешь один раз, а для следующей задачи меняешь только вызов. Кнопка «Решать здесь» открывает редактор, а код каждой задачи сохраняется.

Задачи «Попробуй сам» учебные, в домашке задачи с настоящих экзаменов и из банка ФИПИ. Код в блоках можно менять и запускать, он выполняется прямо в браузере.