Open Source · 7 Languages · 900+ Problems

Project Euler Solutions

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

All Problems

Problem 786: Billiard

View on Project Euler

Project 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

  1. Project Euler problem page: https://projecteuler.net/problem=786
  2. Mertens function: Wikipedia — Mertens function
  3. Möbius inversion formula: Wikipedia — Möbius inversion formula
  4. Euclidean algorithm: Wikipedia — Euclidean algorithm
  5. Lattice point: Wikipedia — Lattice point
  6. Quotient grouping and floor-sum style arguments: Wikipedia — Divisor summatory function

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

Previous: Problem 785 · All Project Euler solutions · Next: Problem 787

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! 🧮