Project Euler Problem 310
Alice and Bob play the game Nim Square.
Solution
Answer: 2586528661783
Let $G(n)$ be the Sprague–Grundy (nimber) value for a heap of size $n$ in Nim Square, where a move consists of removing a square number of stones.
A position $(a,b,c)$ is losing iff
$$G(a)\oplus G(b)\oplus G(c)=0,$$
where $\oplus$ denotes bitwise XOR.
1. Mathematical analysis
For a subtraction game (such as removing square numbers), the Grundy numbers satisfy:
$$G(0)=0,$$
and for $n\ge1$,
$$G(n)=\operatorname{mex} \left( {G(n-k^2)\mid k^2\le n} \right),$$
where mex means the smallest nonnegative integer not in the set.
So we compute $G(n)$ for all $0\le n\le100000$.
Key observation: count by nimber frequencies
Let
$$f(x)=#{n\in[0,N]\mid G(n)=x}.$$
Then the total number of ordered triples $(a,b,c)$ with XOR zero is
$$T = \sum_{x,y} f(x)f(y)f(x\oplus y),$$
because once $G(a)=x$ and $G(b)=y$, the third nimber must be
$$G(c)=x\oplus y.$$
But the problem wants only triples with
$$a\le b\le c.$$
We convert ordered triples into sorted triples.
Let:
- $E$ = all equal triples $(x,x,x)$
- $P$ = ordered triples with exactly two equal
- $D$ = ordered triples with all distinct
Then
$$T=D+P+E.$$
A sorted triple contributes:
- $6$ ordered permutations if all distinct,
- $3$ if exactly two equal,
- $1$ if all equal.
Hence the number of sorted losing positions is
$$L = \frac{D}{6} + \frac{P}{3} + E.$$
Rearranging:
$$L = \frac{T+P+5E}{6}.$$
Computing $P$ and $E$
For all equal:
$$x\oplus x\oplus x=x,$$
so the only losing all-equal triples are heaps with nimber $0$:
$$E=f(0).$$
For exactly two equal:
$$x\oplus x\oplus y=y,$$
so the singleton heap must have nimber $0$.
If $M=N+1$ is the number of heap sizes, then:
$$P = 3,f(0)(M-1),$$
(the factor 3 accounts for which position is the singleton).
We validate the method on the example $N=29$, obtaining:
$$1160,$$
matching the problem statement.
2. Python implementation
import math
def nim_square_losing_positions(N):
# All square moves
squares = [i * i for i in range(1, int(math.isqrt(N)) + 1)]
# Compute Grundy values
grundy = [0] * (N + 1)
max_g = 0
for n in range(1, N + 1):
seen = 0
# Collect reachable Grundy values
for s in squares:
if s > n:
break
seen |= 1 << grundy[n - s]
# mex(seen)
mex = 0
while (seen >> mex) & 1:
mex += 1
grundy[n] = mex
max_g = max(max_g, mex)
# Frequency of each Grundy value
freq = [0] * (max_g + 1)
for g in grundy:
freq[g] += 1
# Count ordered losing triples
ordered_total = 0
for x, fx in enumerate(freq):
for y, fy in enumerate(freq):
z = x ^ y
if z <= max_g:
ordered_total += fx * fy * freq[z]
f0 = freq[0]
M = N + 1
# Exactly two equal
P = 3 * f0 * (M - 1)
# All equal
E = f0
# Convert ordered -> sorted
losing_positions = (ordered_total + P + 5 * E) // 6
return losing_positions
# Check the example
print(nim_square_losing_positions(29)) # 1160
# Solve the problem
print(nim_square_losing_positions(100000))
3. Code walkthrough
- Generate square moves
squares = [i*i for i in range(...)]
Precomputes all legal removals. 2. Compute Grundy values
seen |= 1 << grundy[n - s]
Uses a bitmask to store reachable nimbers efficiently. 3. mex calculation
while (seen >> mex) & 1:
mex += 1
Finds the smallest missing nimber. 4. Frequency table
freq[g] += 1
Counts how many heap sizes have each nimber. 5. Count ordered losing triples
z = x ^ y
ordered_total += fx * fy * freq[z]
Since XOR must be zero. 6. Correct for ordering
Apply:
$$L=\frac{T+P+5E}{6}.$$
Example verification
For $N=29$, the program returns:
$$1160$$
which exactly matches the problem statement.
4. Final verification
- Grundy values up to $100000$ remain small (maximum nimber found: $74$), making the frequency method efficient.
- The sample case matches perfectly.
- The resulting magnitude ($\sim 2.6\times10^{12}$) is reasonable given there are about
$$\binom{100003}{3}\approx 1.67\times10^{14}$$
sorted triples total.
Answer: 2586528661783