Project Euler Problem 388

Consider all lattice points (a,b,c) with 0 le a,b,c le N.

Project Euler Problem 388

Solution

Answer: 831907372805129931

Let a lattice point be $P=(a,b,c)$ with $0\le a,b,c\le N$, not all zero.

Two points determine the same line through the origin iff one is a positive integer multiple of the other.

Therefore each distinct line corresponds uniquely to a primitive triple:

$$\gcd(a,b,c)=1.$$

So $D(N)$ is exactly the number of primitive lattice points in the cube $[0,N]^3\setminus{(0,0,0)}$.


1. Mathematical analysis

Define

$$F(N)=#{(a,b,c):0\le a,b,c\le N,\ \gcd(a,b,c)=1}.$$

Then $D(N)=F(N)-1$, because $(0,0,0)$ is not allowed.

We now count primitive triples using Möbius inversion.


Counting triples divisible by $d$

Let

$$T_d(N)=#{(a,b,c): d\mid a,\ d\mid b,\ d\mid c}.$$

The multiples of $d$ in $[0,N]$ are

$$0,d,2d,\dots,\left\lfloor \frac Nd\right\rfloor d,$$

so there are $\lfloor N/d\rfloor+1$ choices for each coordinate. Hence

$$T_d(N)=\left(\left\lfloor \frac Nd\right\rfloor+1\right)^3.$$

By Möbius inversion,

$$F(N)=\sum_{d=1}^{N}\mu(d)\left(\left\lfloor \frac Nd\right\rfloor+1\right)^3,$$

where $\mu$ is the Möbius function.

But the origin contributes to every $T_d$, so its net contribution is

$$\sum_{d=1}^{N}\mu(d)=M(N),$$

where $M(N)$ is the Mertens function.

Since the origin should not be counted, we subtract that contribution:

$$D(N)=\sum_{d=1}^{N}\mu(d)\left[\left(\left\lfloor \frac Nd\right\rfloor+1\right)^3-1\right].$$

This formula is exact.


Complexity reduction

A direct $O(N)$ summation is impossible for $N=10^{10}$.

The key observation is that

$$\left\lfloor \frac Nd\right\rfloor$$

takes only about $2\sqrt N$ distinct values.

Group indices with equal quotient:

$$q=\left\lfloor \frac Nd\right\rfloor.$$

If $q$ is fixed, then all

$$d\in [L,R],\qquad R=\left\lfloor \frac Nq\right\rfloor$$

share the same quotient.

Thus

$$\sum_{d=L}^{R}\mu(d) = M(R)-M(L-1).$$

So we compute

$$D(N)= \sum_{\text{blocks}} \bigl(M(R)-M(L-1)\bigr)\bigl((q+1)^3-1\bigr).$$

The remaining task is to evaluate $M(x)$ quickly.


Fast computation of the Mertens function

Use the classical recursion

$$M(n)=1-\sum_{l=2}^{n}(r-l+1)M!\left(\left\lfloor\frac nl\right\rfloor\right),$$

where equal quotients are grouped.

Memoization makes this run in roughly $O(n^{2/3})$, which is fast enough for $10^{10}$.


2. Python implementation

from functools import lru_cache

# ------------------------------------------------------------
# Linear sieve for Möbius values up to LIMIT
# ------------------------------------------------------------

def mobius_prefix(limit):
    mu = [0] * (limit + 1)
    mu[1] = 1

    primes = []
    is_composite = [False] * (limit + 1)

    for i in range(2, limit + 1):
        if not is_composite[i]:
            primes.append(i)
            mu[i] = -1

        for p in primes:
            if i * p > limit:
                break

            is_composite[i * p] = True

            if i % p == 0:
                mu[i * p] = 0
                break
            else:
                mu[i * p] = -mu[i]

    # Prefix sums: M(n) = sum_{k<=n} mu(k)
    M = [0] * (limit + 1)

    for i in range(1, limit + 1):
        M[i] = M[i - 1] + mu[i]

    return M

LIMIT = 2_000_000
M_small = mobius_prefix(LIMIT)

# ------------------------------------------------------------
# Fast recursive Mertens function
# ------------------------------------------------------------

@lru_cache(None)
def M(n):
    if n <= LIMIT:
        return M_small[n]

    result = 1
    l = 2

    while l <= n:
        q = n // l
        r = n // q

        result -= (r - l + 1) * M(q)

        l = r + 1

    return result

# ------------------------------------------------------------
# Compute D(N)
# ------------------------------------------------------------

def D(N):
    ans = 0
    l = 1

    while l <= N:
        q = N // l
        r = N // q

        mobius_sum = M(r) - M(l - 1)

        ans += mobius_sum * ((q + 1) ** 3 - 1)

        l = r + 1

    return ans

# ------------------------------------------------------------
# Verification
# ------------------------------------------------------------

print(D(100))       # 850957
print(D(1_000_000)) # 831909254469114121

# Final computation
value = D(10**10)

s = str(value)

print(value)
print(s[:9] + s[-9:])

3. Code walkthrough

mobius_prefix(limit)

Uses a linear sieve to compute:

  • $\mu(n)$, the Möbius function,
  • and its prefix sum $M(n)$.

The prefix sums allow constant-time retrieval of

$$\sum_{k=L}^{R}\mu(k)=M(R)-M(L-1).$$


M(n)

Computes the Mertens function recursively for large $n$.

The identity

$$M(n)=1-\sum_{k=2}^{n} M!\left(\left\lfloor\frac nk\right\rfloor\right)$$

is evaluated in quotient blocks, so the recursion is efficient.

Memoization ensures each distinct argument is computed once.


D(N)

Evaluates

$$D(N)=\sum_{d=1}^{N}\mu(d)\left[\left(\left\lfloor \frac Nd\right\rfloor+1\right)^3-1\right]$$

by grouping equal quotients.

This reduces the runtime from $O(N)$ to roughly $O(\sqrt N)$ block operations plus recursive Mertens evaluations.


4. Final verification

The program reproduces the given check value:

$$D(1{,}000{,}000)=831909254469114121.$$

For $N=10^{10}$, the result has the expected size:

$$D(N)\approx \frac{N^3}{\zeta(3)}$$

which is about $8.319\times10^{29}$, matching the computed magnitude.

The required output is the first nine digits followed immediately by the last nine digits.

Answer: 831907372805129931