Problem 260: Stone Game
View on Project EulerProject Euler Problem 260 Solution
EulerSolve provides an optimized solution for Project Euler Problem 260, Stone Game, with C++, Python, Java, and a step-by-step mathematical explanation.
Problem Summary We work with three heap sizes, written canonically as $0\le x\le y\le z.$ A legal move chooses a positive integer \(n\) and subtracts that same \(n\) from exactly one heap, from any pair of heaps, or from all three heaps, provided no coordinate becomes negative. A position is losing (a P-position) if there is no legal move from it to another losing position. The task is to sum $x+y+z$ over all losing triples with coordinates at most a fixed limit. Mathematical Approach 1. Normal-Play Interpretation This is a finite impartial normal-play game. Every move strictly decreases at least one coordinate, so the game graph is acyclic. Therefore every position is either: 1. losing: no move reaches a losing state, 2. winning: at least one move reaches a losing state. The whole problem is therefore to characterize the losing triples efficiently. 2. Canonical Ordering and Why a Forward Scan Works The code stores each position in sorted order \(x\le y\le z\). After any legal move we sort again, so each game state appears exactly once. If we move from \((x,y,z)\) to a successor and then sort, the new triple is always lexicographically smaller than \((x,y,z)\): at least one coordinate decreases, and no coordinate increases....
Detailed mathematical approach
Problem Summary
We work with three heap sizes, written canonically as
$0\le x\le y\le z.$
A legal move chooses a positive integer \(n\) and subtracts that same \(n\) from exactly one heap, from any pair of heaps, or from all three heaps, provided no coordinate becomes negative.
A position is losing (a P-position) if there is no legal move from it to another losing position. The task is to sum
$x+y+z$
over all losing triples with coordinates at most a fixed limit.
Mathematical Approach
1. Normal-Play Interpretation
This is a finite impartial normal-play game. Every move strictly decreases at least one coordinate, so the game graph is acyclic. Therefore every position is either:
1. losing: no move reaches a losing state,
2. winning: at least one move reaches a losing state.
The whole problem is therefore to characterize the losing triples efficiently.
2. Canonical Ordering and Why a Forward Scan Works
The code stores each position in sorted order \(x\le y\le z\). After any legal move we sort again, so each game state appears exactly once.
If we move from \((x,y,z)\) to a successor and then sort, the new triple is always lexicographically smaller than \((x,y,z)\): at least one coordinate decreases, and no coordinate increases.
Hence a lexicographic scan over all canonical triples is valid: when we decide the status of the current triple, every possible losing successor has already been processed.
The number of canonical states up to limit \(N\) is
$\#\{(x,y,z):0\le x\le y\le z\le N\}=\binom{N+3}{3},$
which is asymptotically \(O(N^3)\).
3. The Seven Move Families
From a canonical triple \((x,y,z)\), every legal move belongs to one of these seven families:
$ (x-n,y,z),\quad (x,y-n,z),\quad (x,y,z-n), $
$ (x-n,y-n,z),\quad (x-n,y,z-n),\quad (x,y-n,z-n),\quad (x-n,y-n,z-n). $
The key observation is that each family preserves a specific 2-dimensional signature. Those preserved quantities are exactly what the fast solver stores.
4. The Seven Preserved Keys
If we reduce only one heap, two heap sizes remain unchanged. If we reduce two or three heaps together, certain differences remain unchanged.
The seven relevant keys are:
$ (y,z),\quad (x,z),\quad (x,y), $
$ (y-x,z),\quad (z-y,x),\quad (z-x,y),\quad (y-x,z-x). $
More concretely:
1. reduce only \(x\): the pair \((y,z)\) is preserved,
2. reduce only \(y\): the pair \((x,z)\) is preserved,
3. reduce only \(z\): the pair \((x,y)\) is preserved,
4. reduce \(x\) and \(y\): \((y-x,z)\) is preserved,
5. reduce \(y\) and \(z\): \((z-y,x)\) is preserved,
6. reduce \(x\) and \(z\): \((z-x,y)\) is preserved,
7. reduce all three: \((y-x,z-x)\) is preserved.
These are exactly the indices used in the arrays one, two, and all.
5. Why Two Losing Positions Cannot Share a Key
Suppose two different losing positions shared one of the seven keys. Then the later one could move to the earlier one by subtracting the difference in the varying coordinate or coordinates.
That is impossible, because a losing position is defined to have no move to another losing position.
So for each key, at most one losing position can occupy it. This is why a Boolean occupancy table is enough; we only need to know whether some earlier losing state already used that key.
6. The Constructive Characterization
The fast algorithm scans triples in lexicographic order and applies the following rule:
1. If any of the seven keys is already occupied by an earlier losing position, then the current triple is winning.
2. If none of the seven keys is occupied, then the current triple is losing.
The reason is a clean induction on scan order.
If a key is occupied, the move family corresponding to that key gives an explicit move to an earlier losing state.
If no key is occupied, then no legal move can reach any earlier losing state, because every legal move must preserve one of those seven keys. Therefore there is no move to a losing state, so the current triple is itself losing.
This proves that the rule is not heuristic; it is exactly equivalent to the recursive P/N-position definition.
7. Small Worked Examples
The base position
$ (0,0,0) $
has no legal move, so it is losing.
The position
$ (0,0,1) $
is winning, because we can subtract \(1\) from the third heap and reach \((0,0,0)\).
A more interesting example is
$ (0,1,2), $
which the code uses as a checkpoint losing state. Relative to the already known losing state \((0,0,0)\), none of its seven keys is occupied, so it is declared losing.
Another checkpoint losing state is
$ (1,3,3). $
For small limits, the first losing states are
$ (0,0,0),\ (0,1,2),\ (1,1,4),\ (1,3,3),\ (0,3,5),\ (2,2,6),\dots $
The code also checks several winning examples such as \((0,0,13)\), \((0,11,11)\), and \((5,5,5)\).
8. Why Three Tables Are Enough
The implementation does not store seven separate structures. Instead it groups them by move type:
1. one[a,b] stores the three single-heap keys \((y,z)\), \((x,z)\), \((x,y)\),
2. two[a,b] stores the three two-heap keys \((y-x,z)\), \((z-y,x)\), \((z-x,y)\),
3. all[a,b] stores the three-heap key \((y-x,z-x)\).
Each table is only \((N+1)\times(N+1)\), so memory stays quadratic rather than cubic.
9. Brute-Force Verification
The helper solve_bruteforce_parallel recomputes losing states directly from the definition. It processes states in layers of constant
$ x+y+z, $
because every legal move strictly decreases that total.
For each state it explicitly asks whether some move reaches an already known losing state. This slow method is used only for checkpoint validation.
The code verifies that the fast solver matches brute force on a small limit, and it also checks the sample value
$ S(100)=173895. $
How the Code Works
The function solve_fast allocates three 2D byte arrays: one, two, and all. It then scans
$ 0\le x\le y\le z\le N $
in nested loops.
For the current triple, it tests the seven table entries corresponding to the seven keys. If any entry is already 1, the triple is winning and contributes nothing. If all seven are 0, the triple is losing: the code adds \(x+y+z\) to the answer and marks those seven entries as occupied.
The auxiliary functions sort_triplet, has_move_to_losing_state, and solve_bruteforce_parallel exist only for validation and checkpoints. The final answer itself is produced by the projection-based scan.
Complexity Analysis
The scan examines exactly
$ \binom{N+3}{3} $
canonical triples, so the running time is \(O(N^3)\).
Each state performs only a constant number of table lookups and writes. Memory usage is dominated by three 2D tables of size \((N+1)^2\), so space is \(O(N^2)\).
The crucial improvement over naive retrograde analysis is that we never enumerate all legal moves from every state inside the fast solver; the seven invariant keys compress that information into constant-time occupancy tests.
Footnotes and References
- Problem page: https://projecteuler.net/problem=260
- Impartial games and P/N positions: Wikipedia - Impartial game
- Normal-play convention: Wikipedia - Normal play convention
- Sprague-Grundy background: Wikipedia - Sprague-Grundy theorem
- Combinations with repetition: Wikipedia - combinations with repetition