Problem 786: Billiard
View on Project EulerProject Euler Problem 786 Solution
EulerSolve provides an optimized solution for Project Euler Problem 786, Billiard, with C++, Python, Java, and a step-by-step mathematical explanation.
Problem Summary As implied by the C++, Python, and Java implementations, the billiard problem is solved by unfolding reflections into straight-line motion on an integer lattice. For an input \(N\), the geometry is compressed into the arithmetic parameter $n=3N+6,$ and the answer becomes a weighted count of primitive lattice directions lying inside a thin strip between two rational boundary lines. The direct geometric search is replaced by Möbius inversion, Mertens-prefix summation, and a fast lattice-point counter. Mathematical Approach Step 1: Reduce the billiard geometry to a strip of lattice points The implementations show that one symmetry sector can be counted through the quantity $q(g)=L(18,10;g)-L(18,30;g),$ where \(L(a,b;c)\) denotes the number of positive integer pairs \((x,y)\) satisfying $ax+by\le c.$ Therefore $q(g)=\#\left\{(x,y)\in\mathbb{Z}_{>0}^2:18x+10y\le g<18x+30y\right\}.$ So \(q(g)\) counts lattice points in a narrow strip: below the line \(18x+10y=g\), but not below the steeper line \(18x+30y=g\). This is the geometric core hidden inside the billiard unfolding....
Detailed mathematical approach
Problem Summary
As implied by the C++, Python, and Java implementations, the billiard problem is solved by unfolding reflections into straight-line motion on an integer lattice. For an input \(N\), the geometry is compressed into the arithmetic parameter
$n=3N+6,$
and the answer becomes a weighted count of primitive lattice directions lying inside a thin strip between two rational boundary lines. The direct geometric search is replaced by Möbius inversion, Mertens-prefix summation, and a fast lattice-point counter.
Mathematical Approach
Step 1: Reduce the billiard geometry to a strip of lattice points
The implementations show that one symmetry sector can be counted through the quantity
$q(g)=L(18,10;g)-L(18,30;g),$
where \(L(a,b;c)\) denotes the number of positive integer pairs \((x,y)\) satisfying
$ax+by\le c.$
Therefore
$q(g)=\#\left\{(x,y)\in\mathbb{Z}_{>0}^2:18x+10y\le g<18x+30y\right\}.$
So \(q(g)\) counts lattice points in a narrow strip: below the line \(18x+10y=g\), but not below the steeper line \(18x+30y=g\). This is the geometric core hidden inside the billiard unfolding.
Step 2: Evaluate the strip count with a Euclidean lattice recursion
For fixed \(a,b,c\), the basic lattice count is
$L(a,b;c)=\sum_{x=1}^{\lfloor c/a\rfloor}\left\lfloor\frac{c-ax}{b}\right\rfloor.$
Assume \(a\ge b\), and write
$m=\left\lfloor\frac{c}{a}\right\rfloor,\qquad k=\left\lfloor\frac{a-1}{b}\right\rfloor,\qquad r=a-kb,\qquad h=\left\lfloor\frac{c-am}{b}\right\rfloor.$
Then the lattice triangle can be decomposed into complete blocks plus a smaller remainder region, giving the recursion
$L(a,b;c)=L(b,r;c-b(km+h))+k\frac{m(m-1)}{2}+mh.$
The base cases are immediate: if \(a>c\), there are no positive solutions; if \(a=b\), then
$L(a,a;c)=\frac{m(m-1)}{2}.$
This is a Euclidean-algorithm descent on the pair \((a,b)\), so the recursion is very shallow. Because the coefficients \(18,10,30\) are fixed, the strip count is effectively constant-time once the formula is known.
Step 3: Convert the Mertens prefix into the exact Möbius weight
Let
$M(x)=\sum_{m\le x}\mu(m)$
be the classical Mertens function, and define the transformed prefix
$A(x)=\sum_{j\ge 0} M\!\left(\left\lfloor\frac{x}{3^j}\right\rfloor\right).$
Its discrete difference is
$A(d)-A(d-1)=\sum_{3^j\mid d}\mu\!\left(\frac{d}{3^j}\right).$
If \(3\nmid d\), only the term \(j=0\) appears, so the difference is simply \(\mu(d)\). If \(3\mid d\), then the only potentially nonzero terms come in the pair \(\mu(u)+\mu(3u)\), which cancel. Hence
$A(d)-A(d-1)= \begin{cases} \mu(d), & 3\nmid d,\\ 0, & 3\mid d. \end{cases}$
This identity explains why the implementations do not sum plain Mertens values: they need the Möbius weight on integers whose common factor is not allowed to contain any prime other than possibly \(3\).
Step 4: Write the final counting formula
With \(n=3N+6\), the reduced-sector count is
$z=\sum_{d=1}^{n}\bigl(A(d)-A(d-1)\bigr)\,q\!\left(\left\lfloor\frac{n}{d}\right\rfloor\right).$
Using the previous step, this can be written more transparently as
$z=\sum_{\substack{1\le d\le n\\3\nmid d}}\mu(d)\,q\!\left(\left\lfloor\frac{n}{d}\right\rfloor\right).$
This is a standard Möbius-inversion pattern: first count all directions in the strip, then remove those that are non-primitive, except that powers of \(3\) remain allowed by the transformed weight. That special treatment of the prime \(3\) is exactly what the summed Mertens transform encodes.
Step 5: Group equal quotients instead of iterating over every divisor
The value \(\lfloor n/d\rfloor\) is constant on intervals. If an interval \([a,b]\) satisfies
$\left\lfloor\frac{n}{d}\right\rfloor=g\qquad\text{for all }d\in[a,b],$
then its total contribution is
$q(g)\sum_{d=a}^{b}\bigl(A(d)-A(d-1)\bigr)=q(g)\bigl(A(b)-A(a-1)\bigr).$
This is why the implementations split the sum into quotient blocks. Only about \(2\sqrt{n}\) distinct quotients occur, so the expensive part is reduced to prefix evaluations and one strip count per block.
Step 6: Restore full billiard symmetry
The implementations finally return
$F(N)=4z+2.$
This shows that the arithmetic sum \(z\) counts one reduced symmetry sector first. The factor \(4\) restores the mirrored sectors, while the final \(+2\) accounts for two exceptional symmetric cases that are handled separately in the normalization.
Worked Example: \(N=10\)
For \(N=10\), we have
$n=3\cdot 10+6=36.$
The smallest lattice point in the strip is \((x,y)=(1,1)\), and it enters when
$18\cdot 1+10\cdot 1=28\le g.$
Therefore \(q(g)=0\) for all \(g<28\). If \(d\ge 2\), then \(\lfloor 36/d\rfloor\le 18\), so every such term vanishes. Only \(d=1\) survives:
$q(36)=1,$
because \((1,1)\) satisfies \(28\le 36<48\). Also \(A(1)-A(0)=\mu(1)=1\). Hence
$z=1,$
and the final count is
$F(10)=4\cdot 1+2=6,$
which matches the checkpoint built into the implementations.
How the Code Works
The C++, Python, and Java implementations all follow the same plan. First they set \(n=3N+6\). Next they memoize the Mertens function \(M(x)\), because the transformed prefix \(A(x)\) repeatedly asks for values of \(M(\lfloor x/3^j\rfloor)\) on overlapping arguments. This turns the Möbius-weight prefix into a cache-friendly arithmetic routine.
For the geometric part, the implementation evaluates \(q(g)\) as the difference of two recursive lattice counters. Each lattice counter repeatedly reduces \((a,b)\) by a Euclidean step, adds the contribution of full triangular blocks, and stops once the remaining region is empty or symmetric. Since the coefficients are fixed, this part is very small compared with the arithmetic prefix work.
The final summation is not performed divisor by divisor. Instead, the implementation walks through the distinct values of \(\lfloor n/d\rfloor\): one loop handles the small divisors directly, and the complementary loop handles the large divisor ranges through interval endpoints. On each block it multiplies a prefix difference of \(A\) by one strip count \(q(g)\). As soon as \(q(g)=0\), the remaining blocks are known to contribute nothing, so the loop can stop early.
Complexity Analysis
The outer summation uses quotient grouping, so only \(O(\sqrt{n})\) blocks are visited instead of all \(n\) divisors. Each strip evaluation uses a Euclidean descent on fixed coefficients, so its cost is effectively constant for this problem. The dominant work is the memoized evaluation of the Mertens prefix on the distinct arguments generated by the quotient blocks and repeated division by \(3\).
So the visible structure is sublinear in \(n\), with memory usage dominated by the Mertens cache. This is far smaller than any direct billiard simulation or naive scan of all candidate lattice directions.
Footnotes and References
- Project Euler problem page: https://projecteuler.net/problem=786
- Mertens function: Wikipedia — Mertens function
- Möbius inversion formula: Wikipedia — Möbius inversion formula
- Euclidean algorithm: Wikipedia — Euclidean algorithm
- Lattice point: Wikipedia — Lattice point
- Quotient grouping and floor-sum style arguments: Wikipedia — Divisor summatory function