Chapter 19
Pick Up Sticks And Help A Frogger Out
Riddler Express
You and a friend play Marienbad. Matchsticks are arranged in four rows of , , , . You alternate turns; on your turn you pick any single row and remove between and all of its remaining sticks. The player who picks up the last stick loses. You go first. Do you win? If so, what is your strategy?
The Riddler, FiveThirtyEight, November 3, 2017(original post)
Solution
You lose against perfect play. The game is the misère version of Nim (the loser takes the last stick), and the starting position is a Player-2 win.
Ordinary Nim. The XOR of pile sizes, called the nim-sum, is the central invariant of Nim. Write each pile in binary, add column-by-column without carries (i.e. XOR), and read the result. With , A position with nim-sum is called a P-position (previous-player wins). The Bouton theorem says: in ordinary Nim (last stick wins) the P-positions are exactly the zero-XOR positions, and any non-zero position can be moved to a zero one in a single legal move.
Misère Nim. The loser takes the last stick. The strategy is the same as ordinary Nim while at least two piles have at least two sticks; once the position consists entirely of singleton piles, the parity rule flips. Specifically: if exactly one pile has sticks, play it so the resulting position is an odd number of singleton piles, so the opponent takes a stick (no choice) and you keep parity. With one pile of sticks the choice between leaving (k singletons) or (k singletons + 1) is yours. A short induction confirms: has XOR zero, so the first player to move from it always faces a non-zero XOR, and as long as multiple multi-stick piles remain, every Player-2 reply returns the position to XOR zero; once at most one multi-stick pile remains, Player ’s singleton parity flip wins.
The losing prognosis for you. You start at . Whatever you do, the resulting nim-sum is non-zero (any move changes exactly one pile’s binary digits, hence changes the XOR). Player has a move to a zero-XOR position; she can keep doing this throughout the multi-multi-pile phase. By the time only one multi-stick pile remains she switches to the misère parity rule and forces you to take the last stick.
The computation
Compute the game value of every position by backward induction. A position is a P-position (current player to move loses) iff every legal move leads to an N-position (next-player-wins); an N-position has at least one move to a P-position. Search the whole game tree from upward.
State: a sorted tuple of pile sizes.
Base: the all-zero position. Whoever’s turn it is cannot move because no sticks remain; but by the rules, the loser took the last stick, so the current player wins (the previous player took the last stick). So is a win-now (the previous mover lost).
Recursion: a position is N (current player wins) iff some legal move yields a P (current-to-move loses for the opponent).
Evaluate .
from functools import lru_cache
@lru_cache(None)
def current_player_wins(state):
# state: sorted tuple of pile sizes. Last stick LOSES.
if sum(state) == 0:
return True # opponent took last stick
for i, p in enumerate(state):
for k in range(1, p + 1): # take k from pile i
new = list(state); new[i] -= k
new = tuple(sorted(new))
if not current_player_wins(new):
return True # move to opp-losing position
return False
print(current_player_wins((1, 3, 5, 7))) # False -> you lose
The recursion declares a loss for the player to move, matching the Nim analysis.
Riddler Classic
A frog needs to jump across lily pads. He starts on the shore (number ) and ends precisely on the last lily pad (number ). He can jump one or two lily pads at a time. How many different ways can he reach his destination? What if he can jump one, two, or three at a time? Or four? Five? Six?
The Riddler, FiveThirtyEight, November 3, 2017(original post)
Solution
Let be the number of distinct ways to reach pad . With jumps of one or two pads, the frog’s final hop onto pad comes either from pad or from pad , and those two families of routes are disjoint, so This is the Fibonacci sequence shifted by one, , giving
If the frog can jump up to pads at a time, the same disjoint-last-jump argument gives the -term recurrence , with and for . Counting routes to pad :
| jump sizes | ways to pad 20 |
|---|---|
Once every jump pattern is allowed, and the total is the number of compositions of , namely .
The computation
Rather than trust the recurrence, enumerate the frog’s routes outright: from the current pad try every legal jump and recurse, counting each sequence of hops that lands exactly on pad . The brute-force totals reproduce the table.
def count_routes(n, k): # every jump sequence summing to n
if n == 0:
return 1 # arrived exactly on the far pad
return sum(count_routes(n - j, k) for j in range(1, min(k, n) + 1))
print(count_routes(20, 2)) # 10946
print([count_routes(20, k) for k in (3, 4, 5, 6)])
print(count_routes(20, 20), 2 ** 19) # 524288, 524288