Open Source · 7 Languages · 900+ Problems

Project Euler Solutions

Complete solutions in C++, Python & Java — with step-by-step mathematical explanations

All Problems

Problem 366: Stone Game III

View on Project Euler

Project Euler Problem 366 Solution

EulerSolve provides an optimized solution for Project Euler Problem 366, Stone Game III, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary Let \(M(n)\) denote the largest winning first move in the Stone Game III position with \(n\) stones, and define $S(N)=\sum_{n=1}^{N} M(n).$ The Project Euler task asks for \(S(10^{18}) \bmod 10^8\). A direct scan of all positions up to \(10^{18}\) is impossible, so the local solution replaces per-position game search by a recurrence on Fibonacci intervals. Mathematical Approach 1. Winning-Move Criterion Used by the Verifier The C++ file contains a slow checker based on Zeckendorf representations. For a positive integer \(m\), let \(z(m)\) be the smallest Fibonacci term appearing in its Zeckendorf decomposition. The checker tests every move \(x\) with \(1 \le x \lt n\) and uses the characterization $2x \lt z(n-x).$ Therefore the game-theoretic quantity implemented in all three language versions is $M(n)=\max\left\{x \in \{1,\dots,n-1\}: 2x \lt z(n-x)\right\}.$ The optimized solver never evaluates this inequality for every \(n\). Instead, it exploits the block pattern that this rule creates when \(n\) is grouped by Fibonacci ranges. 2. Fibonacci Interval Decomposition Use the same Fibonacci indexing as the code: $F_0=1,\qquad F_1=1,\qquad F_{k+1}=F_k+F_{k-1}.$ For each \(k\), define the interval $I_k=[F_k,\,F_{k+1}-1].$ Its length is $|I_k|=F_{k+1}-F_k=F_{k-1}.$ Now let \(A_k\) be the sequence of values \(M(n)\) on \(I_k\)....

Detailed mathematical approach

Problem Summary

Let \(M(n)\) denote the largest winning first move in the Stone Game III position with \(n\) stones, and define

$S(N)=\sum_{n=1}^{N} M(n).$

The Project Euler task asks for \(S(10^{18}) \bmod 10^8\). A direct scan of all positions up to \(10^{18}\) is impossible, so the local solution replaces per-position game search by a recurrence on Fibonacci intervals.

Mathematical Approach

1. Winning-Move Criterion Used by the Verifier

The C++ file contains a slow checker based on Zeckendorf representations. For a positive integer \(m\), let \(z(m)\) be the smallest Fibonacci term appearing in its Zeckendorf decomposition. The checker tests every move \(x\) with \(1 \le x \lt n\) and uses the characterization

$2x \lt z(n-x).$

Therefore the game-theoretic quantity implemented in all three language versions is

$M(n)=\max\left\{x \in \{1,\dots,n-1\}: 2x \lt z(n-x)\right\}.$

The optimized solver never evaluates this inequality for every \(n\). Instead, it exploits the block pattern that this rule creates when \(n\) is grouped by Fibonacci ranges.

2. Fibonacci Interval Decomposition

Use the same Fibonacci indexing as the code:

$F_0=1,\qquad F_1=1,\qquad F_{k+1}=F_k+F_{k-1}.$

For each \(k\), define the interval

$I_k=[F_k,\,F_{k+1}-1].$

Its length is

$|I_k|=F_{k+1}-F_k=F_{k-1}.$

Now let \(A_k\) be the sequence of values \(M(n)\) on \(I_k\). Evaluating the slow definition on small intervals gives

$A_5=(0,1,2,3,1),\qquad A_6=(0,1,2,3,4,5,6,2),$

$A_7=(0,1,\dots,10,3,1),\qquad A_8=(0,1,\dots,16,4,5,6,2).$

This already shows the structure used by the fast solver: each Fibonacci interval starts with a long linear prefix, and the remaining tail is copied recursively from two intervals earlier.

3. Prefix Lengths and the Tail Recurrence

Let \(P_k\) be the length of the initial linear prefix of \(A_k\). Then the first \(P_k\) values in interval \(I_k\) are exactly

$0,1,2,\dots,P_k-1.$

After that prefix comes a tail sequence \(B_k\). The base cases visible in the code are

$B_5=(1),\qquad B_6=(2).$

For \(k \ge 7\), the tail begins with the consecutive block

$P_{k-3},\,P_{k-3}+1,\dots,P_{k-2}-1,$

and after that block it continues with the old tail \(B_{k-2}\).

Comparing lengths gives a recurrence for \(P_k\). Since the full interval has length \(F_{k-1}\), the tail length is \(F_{k-1}-P_k\). On the other hand, the recursive description says that this tail length is

$\left(P_{k-2}-P_{k-3}\right)+\left(F_{k-3}-P_{k-2}\right)=F_{k-3}-P_{k-3}.$

Hence

$F_{k-1}-P_k=F_{k-3}-P_{k-3},$

so

$P_k=F_{k-2}+P_{k-3}\qquad (k \ge 6).$

The implementation uses the initial values

$P_1=1,\qquad P_2=1,\qquad P_3=2,\qquad P_4=3,\qquad P_5=4.$

4. Summing a Full Fibonacci Interval

Let \(T_k\) be the sum of the tail \(B_k\). From the recursive tail structure we get

$T_5=1,\qquad T_6=2,$

and for \(k \ge 7\),

$T_k=T_{k-2}+\sum_{i=P_{k-3}}^{P_{k-2}-1} i.$

Therefore the total contribution of the whole interval \(I_k\) is

$U_k=\sum_{n=F_k}^{F_{k+1}-1} M(n)=\sum_{i=0}^{P_k-1} i + T_k=\frac{(P_k-1)P_k}{2}+T_k.$

This is exactly the quantity stored in interval_sum[k] in the C++ code and in the corresponding arrays in Python and Java.

5. Prefix Queries Inside the Last Interval

For a general limit \(N\), choose the unique \(k\) such that

$F_k \le N \lt F_{k+1},$

and write

$j=N-F_k.$

Then

$S(N)=\sum_{r=1}^{k-1} U_r+\operatorname{Pref}_k(j),$

where \(\operatorname{Pref}_k(j)\) is the sum of the first \(j+1\) entries of \(A_k\).

If \(j \lt P_k\), we are still inside the linear prefix, so

$\operatorname{Pref}_k(j)=\sum_{i=0}^{j} i=\frac{j(j+1)}{2}.$

If \(j \ge P_k\), we first take the whole linear prefix and then continue through the tail. Because the tail itself is built from a consecutive block plus the tail from interval \(k-2\), the prefix calculation descends by \(k \mapsto k-2\). That recursive evaluation is implemented by prefix_interval and prefix_tail.

6. Worked Checkpoint: \(S(100)=728\)

The slow verifier in the C++ program checks this value explicitly. The complete intervals \(I_1\) through \(I_9=[55,88]\) contribute

$U_1+\cdots+U_9=662.$

Now \(100\) lies in \(I_{10}=[89,143]\), so \(j=100-89=11\). Because \(P_{10}=45\), this index is still inside the linear prefix of interval 10. Therefore

$\operatorname{Pref}_{10}(11)=0+1+\cdots+11=\frac{11 \cdot 12}{2}=66.$

Thus

$S(100)=662+66=728,$

matching the checkpoint used by the repository implementation.

How the Code Works

The three solution files implement the same recurrence. They first generate Fibonacci numbers up to \(N\), then precompute the arrays prefix_len, tail_sum, interval_sum, and cumulative interval_prefix_sum. A query \(S(N)\) is answered by locating the last full interval below \(N\), adding the precomputed total before it, and evaluating the remaining partial interval recursively.

The language-specific differences are only numeric types: C++ uses unsigned __int128 for exact sums, Python relies on native big integers, and Java uses BigInteger. The C++ version also keeps the slow Zeckendorf-based verifier, checking \(S(100)=728\) and matching the optimized method against brute force for \(N=1000\).

Complexity Analysis

There are only \(O(\log N)\) Fibonacci levels up to \(N\). Building all tables therefore costs \(O(\log N)\) time and \(O(\log N)\) memory. Answering one query \(S(N)\) also takes \(O(\log N)\) time, because interval lookup and the recursive tail descent both cross only logarithmically many Fibonacci blocks.

Footnotes and References

  1. Problem page: https://projecteuler.net/problem=366
  2. Zeckendorf theorem: Wikipedia — Zeckendorf's theorem
  3. Fibonacci numbers: Wikipedia — Fibonacci number
  4. Arithmetic series: Wikipedia — Arithmetic progression

Mathematical approach · C++ solution · Python solution · Java solution

Previous: Problem 365 · All Project Euler solutions · Next: Problem 367

Need help with a problem? Ask me! 💡
e
✦ Euler GLM 5.2
Hi! I'm Euler. Ask me anything about Project Euler problems, math concepts, or solution approaches! 🧮