Подтема 3 из 6
Программа
Правила игры двумя функциями и группа позиции одной рекурсивной функцией с @cache: один запуск отвечает на все три вопроса.
Правила в коде
Руками мы разобрали игру с порогом 21. На экзамене порог в сотни камней, а куч бывает две — там нужна программа. Как объяснить Python правила игры?
Правила записывают двумя функциями: moves — какие позиции получаются одним ходом, finished — кончилась ли игра. Для нашей игры +1 или ×2 до 21:
def moves(s): return [s + 1, s * 2] def finished(s): return s >= 21 print(moves(10), finished(20), finished(21))Всё, что зависит от условия, живёт в этих двух функциях. Остальная программа одна на все игры темы: для новой задачи переписывают только moves и finished.
any и all
Как записать «есть хотя бы один ход» и «все ходы»?
— верна ли хотя бы одна проверка, all — верны ли все. Из 11 можно получить 12 и 22. Кончает ли игру хоть один ход? А все?
nxt = moves(11)print(nxt)print(any(finished(q) for q in nxt))print(all(finished(q) for q in nxt))Внутри скобок — : для каждой позиции q из nxt проверить finished(q). Это и есть два правила подтемы 1: на своём ходе хватает одного удачного — any, на ходе соперника нужны все — all.
Группа позиции
Как найти группу позиции программой?
Функция g(s) проверяет четыре определения в том же порядке, что и разбор от конца, и возвращает число: 1 — В1, −1 — П1, 2 — В2, −2 — П2, 0 — до победы больше двух ходов. Знак — кто выигрывает: плюс — тот, кто ходит, минус — его соперник. Число — каким по счёту ходом всё решится, как цифра в названии группы.
def moves(s): return [s + 1, s * 2] def finished(s): return s >= 21 def g(s): nxt = moves(s) if any(finished(q) for q in nxt): return 1 # В1 r = [g(q) for q in nxt] if all(x == 1 for x in r): return -1 # П1 if -1 in r: return 2 # В2 if all(x > 0 for x in r): return -2 # П2 return 0 print(g(9))Разберём строки по очереди:
any(finished(q) for q in nxt)— есть ход, который кончает игру: В1.r = [g(q) for q in nxt]— группы всех позиций, которые получит соперник. Здесь функция вызывает саму себя. В этих позициях ходит уже соперник, поэтому 1 в спискеr— это его В1, а не наша.all(x == 1 for x in r)— все ходы ведут в В1: П1.-1 in r— в списке есть −1, то есть ход, после которого соперник в П1: В2.all(x > 0 for x in r)— все ходы ведут в В1 или В2: П2. Положительные числа только у этих двух групп.
Порядок проверок важен. До строки В2 доходят, только если позиция не В1 и не П1, — ровно как в определении. Пройди вызовы g(9) по шагам:
- g(9): ни один ход не кончает игру, поэтому программа спрашивает группы позиций 10 (+1) и 18 (×2).
- g(10): ни один ход не кончает игру — нужны группы позиций 11 (+1) и 20 (×2). g(9) ждёт ответа.
- g(11): ход ×2 даёт 22 — игра кончается. 11 — В1, g(11) = 1.
- g(20): ход +1 даёт 21 — игра кончается. 20 — В1, g(20) = 1.
- g(10): ходы дают [1, 1] — все ходы ведут в В1. 10 — П1, g(10) = −1.
- g(18): ход ×2 даёт 36 — игра кончается. 18 — В1, g(18) = 1.
- g(9): ходы дают [−1, 1] — есть ход в П1. 9 — В2, g(9) = 2. Программа печатает 2.
Отдельного базового случая у функции нет: его роль играет строка В1. Каждый вызов идёт к позициям с большим числом камней, а из большой кучи какой-то ход кончает игру — функция сразу возвращает 1 и дальше не спрашивает. На этом рекурсия и останавливается.
Запоминание
Зачем над функцией пишут @cache?
В одну позицию можно прийти разными путями, и без запоминания g считает её заново каждый раз. В 10 приходят и из 9, и из 5, а с двумя кучами путей к одной позиции десятки.
В задаче темы с двумя кучами функция без запоминания делает почти 90 миллионов вызовов и считает около 40 секунд. С — меньше двух тысяч вызовов, и ответ появляется сразу. Запоминание — две строки:
from functools import cache @cachedef g(s): nxt = moves(s) if any(finished(q) for q in nxt): return 1 # В1 r = [g(q) for q in nxt] if all(x == 1 for x in r): return -1 # П1 if -1 in r: return 2 # В2 if all(x > 0 for x in r): return -2 # П2 return 0Формула функции не меняется: @cache только запоминает ответ для каждой позиции и при повторном вызове берёт его готовым.
Три вопроса
Как получить ответы на задания 19, 20 и 21?
Перебрать все начальные S и отобрать нужную группу: задание 20 — g(s) == 2, задание 21 — g(s) == -2. Задание 19 в форме «при любом ходе Пети» — это g(s) == -1. А для неудачного хода Пети нужна своя проверка: есть ход Пети, после которого Ваня выигрывает сразу.
from functools import cache def moves(s): return [s + 1, s * 2] def finished(s): return s >= 21 @cachedef g(s): nxt = moves(s) if any(finished(q) for q in nxt): return 1 # В1 r = [g(q) for q in nxt] if all(x == 1 for x in r): return -1 # П1 if -1 in r: return 2 # В2 if all(x > 0 for x in r): return -2 # П2 return 0 def mistake(s): # Петя ходит так, что Ваня выигрывает сразу return any(not finished(q) and g(q) == 1 for q in moves(s)) starts = range(1, 21)print(min(s for s in starts if mistake(s)))print([s for s in starts if g(s) == -1])print([s for s in starts if g(s) == 2])print([s for s in starts if g(s) == -2])Это те же числа, что в ручном разборе: 6 для неудачного хода, 10 — П1, 5 и 9 — В2, 8 — П2. Так программу и проверяют: сначала на маленькой игре, где ответ известен, потом меняют правила.
В mistake стоит not finished(q): если ход Пети кончил игру, Петя выиграл — это не ошибка. Диапазон range(1, 21) — все S из условия, от 1 до 20: правая граница в range не входит.
Форма ответа
Программа нашла список значений. Что записать в ответ?
Ровно то, что спрашивает условие: из одного списка берут разные части. Пусть подходящие S собраны в found = [s for s in starts if g(s) == 2]:
- все значения
print(*found)через пробел по возрастанию- наименьшее
min(found)одно число- наибольшее
max(found)одно число- два наименьших
found[:2]первые два — список уже по возрастанию- наименьшее и наибольшее
min(found), max(found)в порядке условия- количество
len(found)сколько значений
Если в ответе два числа, их пишут через пробел в том порядке, что назван в условии: для задания 20 нашей игры — 5 9.
Итого
- Правила
movesиfinished— дословно по условию - anyхотя бы один ход — свой ход
- allвсе ходы — ход соперника
- g(s)1, −1, 2, −2 — В1, П1, В2, П2; 0 — дальше
- @cacheкаждую позицию считаем один раз
- Задания19 —
mistakeили −1, 20 — 2, 21 — −2 - Проверкаигра +1, ×2 до 21: 6, [10], [5, 9], [8]
Проверьте себя
Ответь сначала сам, потом открой ответ.
g(10) в игре +1 или ×2 до 21 и почему?ОтветСкрыть ответ
r = [2, 1, -1]. Что вернёт g?ОтветСкрыть ответ
r = [2, 2]? А если r = [1, 0]?ОтветСкрыть ответ
mistake условие not finished(q)? Что посчитается без него?ОтветСкрыть ответ
g вернёт 1, хотя игра уже кончилась победой Пети.[23, 42, 43, 44], а спрашивают два наименьших значения. Что записать в ответ?ОтветСкрыть ответ
Практика
Для проверки ответов нужен JavaScript. Условия, подсказки и решения доступны ниже.
+1, +3 или ×3 до 200
Два игрока, Петя и Ваня, играют в игру с кучей камней. Игроки ходят по очереди, первый ход делает Петя. За один ход игрок может добавить в кучу один или три камня либо увеличить количество камней в куче в три раза. Игра завершается, когда количество камней в куче становится не менее 200. Победителем считается игрок, сделавший последний ход. В начальный момент в куче было S камней, 1 ≤ S ≤ 199. Укажите такое значение S, при котором Петя не может выиграть за один ход, но при любом ходе Пети Ваня может выиграть своим первым ходом.
Введите число.
Подсказка
Позиция П1: g(s) == -1.
Решение
66: В1 — от 67 (3 · 67 = 201), а из 66 получаются 67, 69 и 198.
from functools import cache def moves(s): return [s + 1, s + 3, s * 3] def finished(s): return s >= 200 @cachedef g(s): nxt = moves(s) if any(finished(q) for q in nxt): return 1 # В1 r = [g(q) for q in nxt] if all(x == 1 for x in r): return -1 # П1 if -1 in r: return 2 # В2 if all(x > 0 for x in r): return -2 # П2 return 0 print(*[s for s in range(1, 200) if g(s) == -1])Ответ: 66
Количество значений L2
Игра: можно добавить два или пять камней либо удвоить кучу, игра завершается, когда в куче не менее 301 камня, 1 ≤ S ≤ 300. Найдите количество таких значений S, при которых одновременно выполняются два условия: у Вани есть выигрышная стратегия, позволяющая ему выиграть первым или вторым ходом при любой игре Пети; у Вани нет стратегии, которая позволит ему гарантированно выиграть первым ходом.
Введите число.
Подсказка
Посчитай позиции П2 во всём диапазоне: len(...).
Решение
3: подходят 142, 143 и 146.
from functools import cache def moves(s): return [s + 2, s + 5, s * 2] def finished(s): return s >= 301 @cachedef g(s): nxt = moves(s) if any(finished(q) for q in nxt): return 1 # В1 r = [g(q) for q in nxt] if all(x == 1 for x in r): return -1 # П1 if -1 in r: return 2 # В2 if all(x > 0 for x in r): return -2 # П2 return 0 print(len([s for s in range(1, 301) if g(s) == -2]))Ответ: 3
Форма ответа задания 20
Игра: можно добавить два или три камня либо удвоить кучу, победа при s ≥ 50, 1 ≤ S ≤ 49. Найдите все S, при которых Петя не может выиграть первым ходом, но выигрывает вторым при любой игре Вани (задание 20). Сколько таких значений, чему равна их сумма, какое наименьшее и какое наибольшее?
Введите четыре числа через пробел: количество, сумму, наименьшее и наибольшее значения.
Подсказка
Сначала собери все В2 в список, затем возьми из него то, что спрашивают.
Решение
4 значения, сумма 75, наименьшее 12, наибольшее 22.
from functools import cache def moves(s): return [s + 2, s + 3, s * 2] def finished(s): return s >= 50 @cachedef g(s): nxt = moves(s) if any(finished(q) for q in nxt): return 1 # В1 r = [g(q) for q in nxt] if all(x == 1 for x in r): return -1 # П1 if -1 in r: return 2 # В2 if all(x > 0 for x in r): return -2 # П2 return 0 found = [s for s in range(1, 50) if g(s) == 2]print(len(found), sum(found), min(found), max(found))Ответ: 4 75 12 22
Два наименьших значения
Игра та же: можно добавить в кучу один или три камня либо утроить её; игра завершается, когда в куче не менее 200 камней; 1 ≤ S ≤ 199. Найдите два наименьших значения S, при которых у Пети есть выигрышная стратегия, причём одновременно выполняются два условия: Петя не может выиграть за один ход; Петя может выиграть своим вторым ходом независимо от того, как будет ходить Ваня.
Введите два числа через пробел в порядке возрастания.
Подсказка
Подходящих значений больше двух — возьми два наименьших.
Решение
22 и 63: подходят 22, 63 и 65; из 22 утроение даёт 66 (П1), из 63 и 65 — прибавление.
from functools import cache def moves(s): return [s + 1, s + 3, s * 3] def finished(s): return s >= 200 @cachedef g(s): nxt = moves(s) if any(finished(q) for q in nxt): return 1 # В1 r = [g(q) for q in nxt] if all(x == 1 for x in r): return -1 # П1 if -1 in r: return 2 # В2 if all(x > 0 for x in r): return -2 # П2 return 0 found = [s for s in range(1, 200) if g(s) == 2]print(*found[:2])Ответ: 22 63