Problem 388: Distinct Lines
View on Project EulerProject Euler Problem 388 Solution
EulerSolve provides an optimized solution for Project Euler Problem 388, Distinct Lines, with C++, Python, Java, and a step-by-step mathematical explanation.
Problem Summary In the implementation used in this repository, a distinct line is represented by a nonzero lattice direction \(P=(a,b,c)\) with \(0\le a,b,c\le N\). The line is the one passing through the origin and \(P\). Two lattice points describe the same line exactly when one is a positive integer multiple of the other, so each line has a unique primitive representative whose coordinates have gcd equal to 1. Therefore the quantity being computed is $$D(N)=\#\left\{(a,b,c)\in \{0,\dots,N\}^3\setminus\{(0,0,0)\}:\gcd(a,b,c)=1\right\}.$$ The solutions then print the exact decimal value if it has at most 18 digits, or the concatenation of the first 9 and last 9 digits when the full answer is much longer. Mathematical Approach Step 1: Distinct Lines Become Primitive Triples If \(g=\gcd(a,b,c)\gt 1\), then \((a,b,c)=g(a',b',c')\) with \(\gcd(a',b',c')=1\). The points \((a,b,c)\) and \((a',b',c')\) lie on the same line through the origin, so the non-primitive vector is redundant. Conversely, every primitive triple inside the box gives one distinct line. Because the coordinates are restricted to \(\{0,\dots,N\}\), there is no sign ambiguity: each line contributes exactly one primitive direction vector....
Detailed mathematical approach
Problem Summary
In the implementation used in this repository, a distinct line is represented by a nonzero lattice direction \(P=(a,b,c)\) with \(0\le a,b,c\le N\). The line is the one passing through the origin and \(P\). Two lattice points describe the same line exactly when one is a positive integer multiple of the other, so each line has a unique primitive representative whose coordinates have gcd equal to 1.
Therefore the quantity being computed is
$$D(N)=\#\left\{(a,b,c)\in \{0,\dots,N\}^3\setminus\{(0,0,0)\}:\gcd(a,b,c)=1\right\}.$$
The solutions then print the exact decimal value if it has at most 18 digits, or the concatenation of the first 9 and last 9 digits when the full answer is much longer.
Mathematical Approach
Step 1: Distinct Lines Become Primitive Triples
If \(g=\gcd(a,b,c)\gt 1\), then \((a,b,c)=g(a',b',c')\) with \(\gcd(a',b',c')=1\). The points \((a,b,c)\) and \((a',b',c')\) lie on the same line through the origin, so the non-primitive vector is redundant. Conversely, every primitive triple inside the box gives one distinct line. Because the coordinates are restricted to \(\{0,\dots,N\}\), there is no sign ambiguity: each line contributes exactly one primitive direction vector.
Step 2: Möbius Inversion Enforces \(\gcd(a,b,c)=1\)
Use the standard coprimality indicator
$$\mathbf{1}_{\gcd(a,b,c)=1}=\sum_{d\mid \gcd(a,b,c)} \mu(d),$$
where \(\mu\) is the Möbius function. Summing this identity over all triples in the box yields
$$D(N)=\sum_{0\le a,b,c\le N}\mathbf{1}_{(a,b,c)\neq(0,0,0)}\sum_{d\mid \gcd(a,b,c)}\mu(d).$$
Now exchange the order of summation. For a fixed divisor \(d\), the condition \(d\mid a,b,c\) means that each coordinate can be any multiple of \(d\) up to \(N\). That gives \(\left\lfloor N/d\right\rfloor+1\) choices per coordinate, including 0. The all-zero triple must then be removed exactly once.
Hence
$$\boxed{D(N)=\sum_{d=1}^{N}\mu(d)\left(\left(\left\lfloor\frac{N}{d}\right\rfloor+1\right)^3-1\right).}$$
This is the core formula implemented in the C++, Python, and Java files.
Step 3: Quotient Blocks Compress the Outer Sum
A naive loop over every \(d\le N\) is too slow when \(N=10^{10}\). The crucial observation is that the quotient
$$q=\left\lfloor\frac{N}{d}\right\rfloor$$
stays constant on whole intervals. If a block starts at \(l\), then every \(d\) in
$$l\le d\le r=\left\lfloor\frac{N}{q}\right\rfloor$$
has the same quotient \(q\). Over that interval the cubic factor is constant, so only the Möbius prefix sum changes. Introduce the Mertens function
$$M(x)=\sum_{n\le x}\mu(n).$$
Then for one block,
$$\sum_{d=l}^{r}\mu(d)=M(r)-M(l-1),$$
and the full sum becomes a block sum of the form
$$D(N)=\sum \left((q+1)^3-1\right)\left(M(r)-M(l-1)\right),$$
where the summation runs over all quotient blocks. The number of such blocks is only about \(2\sqrt N\), which is why the outer loop is practical.
Step 4: Fast Evaluation of the Mertens Prefix
The source files use the same two-level plan. First, they build \(\mu(d)\) with a linear sieve up to
$$B=\max\left(10^6,\left\lfloor N^{2/3}\right\rfloor+1000\right).$$
From this sieve they build the prefix array \(M(1),M(2),\dots,M(B)\).
For larger arguments, they use the classical identity
$$\sum_{d=1}^{x}\mu(d)\left\lfloor\frac{x}{d}\right\rfloor=1,$$
which can be rewritten as
$$M(x)=1-\sum_{k=2}^{x} M\left(\left\lfloor\frac{x}{k}\right\rfloor\right).$$
Again, equal floor quotients are grouped, so one recursive subtraction handles a whole interval at once. Large values are cached after the first computation, exactly as the `cache` structure does in the implementations.
Worked Checkpoints
For \(N=1\), every nonzero triple in \(\{0,1\}^3\) is primitive, so
$$D(1)=7.$$
For \(N=2\), the Möbius formula already shows the mechanism:
$$D(2)=\mu(1)\left((2+1)^3-1\right)+\mu(2)\left((1+1)^3-1\right)=26-7=19.$$
The C++ code also checks the optimized result against brute force at \(N=40\), where both methods give
$$D(40)=56335.$$
A larger hard-coded checkpoint is
$$D(10^6)=831909254469114121,$$
and only after these tests does the program attempt the target \(N=10^{10}\).
How the Code Works
The C++ file is the authoritative implementation. The function mu_sieve constructs the linear Möbius
sieve, the class MertensPrefix serves \(M(x)\) either from the prefix array or from the memoized
recurrence, and distinct_lines performs the quotient-block accumulation. The main program stores the
running total in __int128, exposes --n= and --skip-checkpoints, and formats
the final output through first9_last9_token.
The Python file is a compact fixed-\(N\) version of the same mathematics and relies on Python's arbitrary-precision
integers. The Java file mirrors the same algorithm, but uses BigInteger for the cubic block value and
the final accumulated answer because Java has no built-in signed 128-bit integer type.
Complexity Analysis
Let \(B=\max\left(10^6,\left\lfloor N^{2/3}\right\rfloor+1000\right)\). Building the linear sieve and the prefix Mertens table costs \(O(B)\) time and \(O(B)\) memory. The outer distinct-lines sum uses \(O(\sqrt N)\) quotient blocks. Large Mertens arguments are memoized and are themselves processed in grouped floor-quotient intervals, so the whole method is far below \(O(N)\) work and is practical for the implemented target \(N=10^{10}\).
Footnotes and References
- Problem page: https://projecteuler.net/problem=388
- Möbius inversion formula: Wikipedia — Möbius inversion formula
- Möbius function: Wikipedia — Möbius function
- Mertens function: Wikipedia — Mertens function
- Quotient grouping and floor-sum methods: Wikipedia — Dirichlet hyperbola method
Mathematical approach · C++ solution · Python solution · Java solution
Previous: Problem 387 · All Project Euler solutions · Next: Problem 389