Две функции win и lose. «Выигрывает» не свойство позиции: в позиции (17, 56) выигрывает тот, чья очередь ходить, и проигрывает тот, кто только что сходил. У каждой позиции два ответа, и какой из них нужен, решает не она, а то, чей сейчас ход. Передача хода сопернику – это переход в другую функцию, поэтому рекурсия тут взаимная: win зовёт lose, lose зовёт win.
win(pos, k) говорит про того, чья очередь ходить: «я выиграю не более чем за k своих ходов». Ему хватает одного удачного хода, поэтому цикл выходит по первому же успеху – return True. Это дословный перевод фразы из условия «игрок может выиграть»: может, значит существует ход.
lose(pos, k) говорит про него же обратное: «что бы я ни сделал, соперник закончит не позже своего k-го хода». Здесь одного хода мало, нужны все: любой контрпример – и позиция уже не проигрышная, поэтому цикл выходит по первой же неудаче – return False. Это перевод фразы «при любых ходах противника».
Отсюда и k – запас собственных ходов того, о ком идёт речь. В win игрок тратит ход и передаёт очередь: у соперника позиция nxt, а у него самого остаётся k - 1, поэтому вызывается lose(nxt, k - 1). В lose наоборот: ход переходит сопернику, который ещё ничего не потратил, поэтому win(nxt, k) – с тем же k. Пара строк if over(nxt) перед этим – это конец игры: тот, кто сходил в такую позицию, уже победил, и заглядывать дальше незачем.
Рекурсия конечна не потому, что кучи растут, а потому, что убывает k. Он уменьшается через уровень – в переходе win → lose, – так что глубина не превышает $2k$, а k в этих заданиях равен 1, 2 или 3. Дно – строка if k == 0: return False в win: ходов больше нет, выигрывать нечем. Отдельного дна у lose нет и не нужно: lose(pos, 0) сведётся к win(nxt, 0), то есть к False, и вернёт False – проиграть за ноль ходов соперника действительно нельзя, ему нужен хотя бы один.
Как это разворачивается. Возьмём S = 28 – первый ответ задания 20 – и посмотрим, что делает win((17, 28), 2). Ходит Петя, в кучах 45 камней, до порога далеко, поэтому ни один ход игру не заканчивает и каждый проверяется через lose(..., 1) – «попадёт ли Ваня в позицию, из которой проигрывает».
lose((21, 28), 1) → False: Ваня спасается ходом в (25, 28), откуда Петя дотягивается лишь до 81 при пороге 133
lose((17, 32), 1) → False: и здесь у Вани находится ответ, после которого Петя не добивает
lose((34, 28), 1) → False: и здесь у Вани находится ответ, после которого Петя не добивает
lose((17, 56), 1) → True: что бы Ваня ни ответил, у Пети есть добивающий ход:
- Ваня ходит в (21, 56), Петя – в (21, 112), сумма 133
- Ваня ходит в (17, 60), Петя – в (17, 120), сумма 137
- Ваня ходит в (34, 56), Петя – в (34, 112), сумма 146
- Ваня ходит в (17, 112), Петя – в (21, 112), сумма 133
Три первые ветки вернули False, четвёртая – True, значит и win((17, 28), 2) истинно. А win((17, 28), 1) ложно: одним ходом Петя добирается самое большее до 73, а порог – 133. Это и есть вторая половина условия задания 20: «Петя не может выиграть за один ход».
Важно, в каком порядке это считается: win не строит всё дерево заранее, а идёт по ходам слева направо и останавливается на первом, который вернул True. Поэтому до последней ветки он доходит, только если три первые провалились.
И последнее – откуда в программе берутся сами вопросы. Каждое слово условия превращается в одну деталь вызова:
| Слово в условии |
Что это в коде |
| «может выиграть», «есть выигрышная стратегия» |
win: достаточно одного хода |
| «при любых ходах противника», «независимо от того, как будет ходить» |
lose: подойти должны все ходы |
| «своим первым ходом» |
k = 1 |
| «своим вторым ходом» |
k = 2 |
| «не может выиграть за один ход» |
not win(pos, 1) |
| «первый ход делает Петя» |
из начальной позиции вызываем функцию про Петю: win – если спрашивают про него, lose – если про Ваню |
Ровно поэтому три задания решаются одной программой: меняется не код, а строка с вызовом.