Подтема 5 из 6
Уменьшение кучи и ходы с условием
Кучу уменьшают вычитанием и делением с округлением, ходы разрешены по условию; как найти наибольшее S, когда верхней границы нет.
Уменьшение кучи
Что меняется, когда кучу уменьшают?
Только правила: ходы уменьшают кучу, а выигрывает тот, кто первым опустился до порога. Функция g прежняя. Учебная игра: можно убрать один камень или уменьшить кучу вдвое с округлением вниз; выигрывает тот, у кого после хода в куче не более 10 камней; S ≥ 11.
def moves(s): return [s - 1, s // 2] def finished(s): return s <= 10 print(moves(21), moves(22))// — : 21 // 2 = 10, поэтому из 21 выигрывают сразу. А из 22 делением получится 11 — игра продолжается.
Группы позиций
Где здесь В1, П1, В2 и П2?
Определения те же, только конец игры теперь внизу: ближе к победе — меньше камней, и перебор идёт вверх от порога. В1 — 11–21. П1 — 22: оба хода, в 21 и в 11, ведут в В1. В ловушку 22 ведут 23 (−1), 44 и 45 (44 // 2 = 45 // 2 = 22) — это В2. П2 — 24 и 46.
Программа находит то же:
from functools import cache def moves(s): return [s - 1, s // 2] def finished(s): return s <= 10 @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 starts = range(11, 60)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])Округление
Как записать «уменьшить в два раза» в коде?
Так, как сказано в условии: округление вниз, вверх или «только если делится нацело» — это три разных хода.
- с округлением вниз
s // 221 // 2 = 10- с округлением вверх
(s + 1) // 2(21 + 1) // 2 = 11- если делится нацело
if s % 2 == 021 на 2 не делится — хода нет
Обычное деление и round не подходят: 21 / 2 даёт дробное 10.5, а round(10.5) — 10, потому что округляет к чётному:
print(21 // 2, (21 + 1) // 2, 21 / 2, round(10.5))Деление на 3 с округлением вверх — (s + 2) // 3: к числу добавляют делитель без единицы.
Граница перебора
В условии «S ≥ 11», а сверху границы нет. До какого S перебирать?
Наименьшее S ищут вверх от порога, пока не найдётся. Для наибольшего нужна граница из рассуждения: дальше неё ответов быть не может. Найдём наибольшее S, при котором Ваня может выиграть первым ходом после неудачного хода Пети.
Быстрее всего куча убывает от двух делений подряд: из S получается S // 4. Чтобы Ваня дошёл до порога, нужно S // 4 ≤ 10, а это верно только до S = 43: 43 // 4 = 10, но 44 // 4 = 11. Значит, ответ не больше 43, и перебора до 1000 хватит с запасом:
from functools import cache def moves(s): return [s - 1, s // 2] def finished(s): return s <= 10 @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)) print(max(s for s in range(11, 1000) if mistake(s)))Если спрашивают о гарантии — «при любом ходе Пети Ваня выигрывает первым ходом», — граница другая. Ваня должен выиграть даже после самого медленного хода Пети, здесь это −1. Значит, S − 1 должно быть В1, то есть S − 1 ≤ 21 и S ≤ 22. Дальше П1 быть не может.
Тонкости:очень большие S
В экзаменационных задачах порог бывает в десятки тысяч камней. Перебирай S вверх от порога: тогда для каждого нового S позиции ниже уже запомнены, и рекурсия неглубокая. Если начать сразу с огромного S, цепочка вызовов −1, −1, −1… упрётся в .
Ходы с условием
Как записать ход, который разрешён не всегда?
В moves ход добавляют только после проверки условия. Пусть делить пополам можно только чётную кучу:
from functools import cache def moves(s): result = [s - 1] if s % 2 == 0: result.append(s // 2) return result def finished(s): return s <= 10 @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(moves(13), moves(14))print([s for s in range(11, 30) if g(s) == -1])Условие меняет все группы. Без него 13 была В1 (13 // 2 = 6), а теперь из 13 один ход — в 12, а это В1 соперника. Так же пишут «если кратно трём», «добавить делитель числа», «добавлять только в меньшую кучу».
Запрещённый результат тоже убирают в moves. «Превышать 21 нельзя» — [q for q in (s + 1, s * 2) if q <= 21], а finished тогда проверяет s == 21.
Позиция без ходов
Что делает g, если из позиции нет ни одного хода?
Возвращает −1, и это обычно ошибка: для пустого списка all даёт True. Пусть к куче можно добавить её делитель d, 1 < d < s, а выигрывает получивший не менее 30 камней. У простого числа таких делителей нет:
from functools import cache def moves(s): return [s + d for d in range(2, s) if s % d == 0] def finished(s): return s >= 30 @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(moves(12), moves(7), g(7))Из 7 Петя не может сходить вовсе, а программа назвала 7 позицией П1: ни одного хода — значит, «все ходы ведут в В1». верен, потому что нарушений нет. Что значит позиция без ходов, решает условие. Если она не должна попадать в ответ, добавь в начало g строку if not nxt: return 0 — сразу после nxt = moves(s).
Итого
- Уменьшениете же группы; перебор вверх от порога
- Вниз
s // 2 - Вверх
(s + 1) // 2 - Наибольшее Sграница — самый быстрый спуск до порога
- Ход с условиемдобавить в
movesпослеif - Нет ходов
all([])— True: решить по условию
Проверьте себя
Ответь сначала сам, потом открой ответ.
ОтветСкрыть ответ
ОтветСкрыть ответ
ОтветСкрыть ответ
(s + 2) // 3: например, 10 → 4, а 9 → 3.moves(15) в игре «−1 или //2»?ОтветСкрыть ответ
[14]: 15 нечётное, деления нет.ОтветСкрыть ответ
Практика
Для проверки ответов нужен JavaScript. Условия, подсказки и решения доступны ниже.
Уменьшение кучи: задание 19
Два игрока, Петя и Ваня, играют в игру с кучей камней. Игроки ходят по очереди, первый ход делает Петя. За один ход игрок может убрать из кучи два камня, убрать пять камней или уменьшить количество камней в куче в три раза (количество камней, полученное при делении, округляется до меньшего). Игра завершается, когда количество камней в куче становится не более 30. Победителем считается игрок, сделавший последний ход. В начальный момент в куче было S камней, S ≥ 31. Укажите минимальное значение S, при котором Петя не может выиграть за один ход, но при любом ходе Пети Ваня может выиграть своим первым ходом.
Введите число.
Подсказка
В1 — до 92: 92 // 3 = 30. Ищи первую П1 выше.
Решение
93: В1 — от 31 до 92 (92 // 3 = 30), а из 93 получаются 91, 88 и 31 — все В1.
from functools import cache def moves(s): return [s - 2, s - 5, s // 3] def finished(s): return s <= 30 @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(min(s for s in range(31, 1000) if g(s) == -1))Ответ: 93
Уменьшение кучи: задание 20
Игра та же: можно убрать из кучи два или пять камней либо уменьшить кучу в три раза с округлением вниз; игра завершается, когда в куче не более 30 камней; S ≥ 31. Найдите два наименьших значения S, при которых у Пети есть выигрышная стратегия, причём одновременно выполняются два условия: Петя не может выиграть за один ход; Петя может выиграть своим вторым ходом независимо от того, как будет ходить Ваня.
Введите два числа через пробел в порядке возрастания.
Подсказка
В2: есть ход в П1.
Решение
95 и 96: ход −2 ведёт в 93 и 94 — П1.
from functools import cache def moves(s): return [s - 2, s - 5, s // 3] def finished(s): return s <= 30 @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(31, 1000) if g(s) == 2][:2])Ответ: 95 96
Уменьшение кучи: задание 21
Игра та же: можно убрать из кучи два или пять камней либо уменьшить кучу в три раза с округлением вниз; игра завершается, когда в куче не более 30 камней; S ≥ 31. Найдите минимальное значение S, при котором одновременно выполняются два условия: у Вани есть выигрышная стратегия, позволяющая ему выиграть первым или вторым ходом при любой игре Пети; у Вани нет стратегии, которая позволит ему гарантированно выиграть первым ходом.
Введите число.
Подсказка
П2: все ходы отдают В1 или В2, но не все — В1.
Решение
97 — наименьшая П2.
from functools import cache def moves(s): return [s - 2, s - 5, s // 3] def finished(s): return s <= 30 @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(min(s for s in range(31, 1000) if g(s) == -2))Ответ: 97
Граница деления
Игра заканчивается при s ≤ 12, разрешено деление на 3 с округлением вниз. Какое наибольшее количество камней можно превратить в победную позицию одним делением?
Введите число.
Подсказка
Найди последнее число, которое после деления на 3 с округлением вниз ещё не больше 12.
Решение
38: 38 // 3 = 12, а 39 // 3 = 13.
Решение программой
print(max(s for s in range(1, 100) if s // 3 <= 12))Ответ: 38
Наибольшее S после неудачного хода
Игра: убрать три камня или разделить кучу на 4 с округлением вниз, выигрывает получивший не более 20 камней, S ≥ 21. Найди наибольшее S, при котором Ваня может выиграть первым ходом после неудачного хода Пети, и объясни, почему дальше перебирать не нужно.
Введите число.
Подсказка
Два деления подряд дают S // 16 — отсюда граница перебора.
Решение
335: два деления подряд дают S // 16 ≤ 20, то есть S ≤ 335; из 336 даже самый быстрый спуск оставляет 21. Партия 335 → 83 → 20.
from functools import cache def moves(s): return [s - 3, s // 4] def finished(s): return s <= 20 @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)) print(max(s for s in range(21, 1000) if mistake(s)))Ответ: 335
Наибольшее S при неудачном ходе
Игра та же: можно убрать из кучи два или пять камней либо уменьшить кучу в три раза с округлением вниз; игра завершается, когда в куче не более 30 камней; S ≥ 31. Укажите максимальное значение S, при котором Ваня может выиграть своим первым ходом после неудачного первого хода Пети.
Введите число.
Подсказка
Два деления подряд: S // 9 ≤ 30 — это граница.
Решение
278: S // 9 ≤ 30 даёт S ≤ 278; партия 278 → 92 → 30.
from functools import cache def moves(s): return [s - 2, s - 5, s // 3] def finished(s): return s <= 30 @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)) print(max(s for s in range(31, 1000) if mistake(s)))Ответ: 278