All Euler problems
Project Euler

Distinct Lines through Lattice Points

Consider all lattice points (a, b, c) with 0 <= a, b, c <= N. From the origin O(0,0,0) a straight line is drawn to every point. Let D(N) be the number of distinct such lines. You are given that D(1...

Source sync Apr 19, 2026
Problem #0388
Level Level 19
Solved By 695
Languages C++, Python
Answer 831907372805129931
Length 345 words
number_theorygeometrylinear_algebra

Problem Statement

This archive keeps the full statement, math, and original media on the page.

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

From the origin \(O(0,0,0)\) all lines are drawn to the other lattice points.

Let \(D(N)\) be the number of distinct such lines.

You are given that \(D(1\,000\,000) = 831909254469114121\).

Find \(D(10^{10})\). Give as your answer the first nine digits followed by the last nine digits.

Problem 388: Distinct Lines through Lattice Points

Mathematical Analysis

Two lattice points (a1,b1,c1)(a_1, b_1, c_1) and (a2,b2,c2)(a_2, b_2, c_2) define the same line through the origin if and only if one is a positive scalar multiple of the other. That is, (a1,b1,c1)=k(a2,b2,c2)(a_1, b_1, c_1) = k(a_2, b_2, c_2) for some positive rational kk.

Equivalently, each distinct line corresponds to a unique primitive direction: a point (a,b,c)(a, b, c) in {0,,N}3{(0,0,0)}\{0, \ldots, N\}^3 \setminus \{(0,0,0)\} with gcd(a,b,c)=1\gcd(a, b, c) = 1.

Therefore:

D(N)=#{(a,b,c){0,,N}3{(0,0,0)}:gcd(a,b,c)=1}D(N) = \#\{(a,b,c) \in \{0,\ldots,N\}^3 \setminus \{(0,0,0)\} : \gcd(a,b,c) = 1\}

Derivation / Algorithm

Mobius Inversion Formula

Using the identity [gcd(a,b,c)=1]=dgcd(a,b,c)μ(d)[\gcd(a,b,c) = 1] = \sum_{d \mid \gcd(a,b,c)} \mu(d), we get:

D(N)=(a,b,c){0,,N}3(a,b,c)(0,0,0)dgcd(a,b,c)μ(d)D(N) = \sum_{\substack{(a,b,c) \in \{0,\ldots,N\}^3 \\ (a,b,c) \neq (0,0,0)}} \sum_{d \mid \gcd(a,b,c)} \mu(d)

Swapping summation order and substituting a=daa = da', b=dbb = db', c=dcc = dc':

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

The +1+1 arises because each coordinate in {0,d,2d,}\{0, d, 2d, \ldots\} contributes N/d+1\lfloor N/d \rfloor + 1 values. The 1-1 subtracts the origin.

Sub-linear Computation via Mertens Function

For large NN (e.g., N=1010N = 10^{10}), we cannot sieve μ(d)\mu(d) for all dNd \le N. Instead, we group terms by q=N/dq = \lfloor N/d \rfloor:

D(N)=distinct qg(q)(M(N/q)M(N/(q+1)))D(N) = \sum_{\text{distinct } q} g(q) \cdot \bigl(M(\lfloor N/q \rfloor) - M(\lfloor N/(q+1) \rfloor)\bigr)

where g(q)=(q+1)31g(q) = (q+1)^3 - 1 and M(x)=d=1xμ(d)M(x) = \sum_{d=1}^{x} \mu(d) is the Mertens function.

There are only O(N)O(\sqrt{N}) distinct values of N/d\lfloor N/d \rfloor. The Mertens function is computed using the recursive identity:

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

with memoization and a sieve for small values up to threshold N2/3N^{2/3}, giving an overall time complexity of O(N2/3)O(N^{2/3}).

Proof of Correctness

  1. Bijection: Each distinct line through the origin and a point in {0,,N}3\{0,\ldots,N\}^3 corresponds to a unique primitive direction vector (a,b,c)(a,b,c) with gcd(a,b,c)=1\gcd(a,b,c) = 1. Every non-primitive point (a,b,c)(a,b,c) with g=gcd(a,b,c)>1g = \gcd(a,b,c) > 1 lies on the same line as (a/g,b/g,c/g)(a/g, b/g, c/g), which is primitive and also in the cube since a/gaNa/g \le a \le N.

  2. Mobius inversion: The standard identity dnμ(d)=[n=1]\sum_{d \mid n} \mu(d) = [n = 1] correctly extracts the coprimality condition.

  3. Verification: D(1000000)=831909254469114121D(1\,000\,000) = 831\,909\,254\,469\,114\,121 matches the problem’s given value. Small cases (D(1)=7D(1) = 7, D(2)=19D(2) = 19, etc.) are verified against brute force.

Correctness

Theorem. The method described above computes exactly the quantity requested in the problem statement.

Proof. The preceding analysis identifies the admissible objects and derives the formula, recurrence, or exhaustive search carried out by the algorithm. The computation evaluates exactly that specification, so every valid contribution is included once and no invalid contribution is counted. Therefore the returned value is the required answer. \square

Complexity Analysis

  • Sieve phase: O(N2/3)O(N^{2/3}) time and space to compute μ\mu and prefix sums up to threshold.
  • Mertens recursion: O(N2/3)O(N^{2/3}) time with memoization over O(N)O(\sqrt{N}) distinct values.
  • Main summation: O(N)O(\sqrt{N}) iterations.
  • Overall: O(N2/3)O(N^{2/3}) time, O(N2/3)O(N^{2/3}) space.

Answer

831907372805129931\boxed{831907372805129931}

Code

Each problem page includes the exact C++ and Python source files from the local archive.

C++ project_euler/problem_388/solution.cpp
#include <bits/stdc++.h>
using namespace std;

/*
 * Problem 388: Distinct Lines through Lattice Points
 *
 * D(N) = sum_{d=1}^{N} mu(d) * ((floor(N/d)+1)^3 - 1)
 *
 * Sub-linear computation via Mertens function M(x) = sum_{d<=x} mu(d).
 * Time: O(N^{2/3}), Space: O(N^{2/3}).
 */

using i64 = long long;
using i128 = __int128;

const i64 N = 10000000000LL;  // 10^10
int THRESH;

vector<int> mu_arr;
vector<i64> M_small;
unordered_map<i64, i64> M_cache;

void sieve_mu() {
    mu_arr.assign(THRESH + 1, 0);
    mu_arr[1] = 1;
    vector<bool> is_prime(THRESH + 1, true);
    vector<int> primes;
    for (int i = 2; i <= THRESH; i++) {
        if (is_prime[i]) {
            primes.push_back(i);
            mu_arr[i] = -1;
        }
        for (int p : primes) {
            if ((i64)i * p > THRESH) break;
            is_prime[i * p] = false;
            if (i % p == 0) {
                mu_arr[i * p] = 0;
                break;
            }
            mu_arr[i * p] = -mu_arr[i];
        }
    }
    M_small.assign(THRESH + 1, 0);
    for (int i = 1; i <= THRESH; i++)
        M_small[i] = M_small[i - 1] + mu_arr[i];
}

i64 mertens(i64 n) {
    if (n <= THRESH) return M_small[n];
    auto it = M_cache.find(n);
    if (it != M_cache.end()) return it->second;

    i64 result = 1;
    i64 k = 2;
    while (k <= n) {
        i64 q = n / k;
        i64 k_next = n / q + 1;
        result -= (k_next - k) * mertens(q);
        k = k_next;
    }
    M_cache[n] = result;
    return result;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    // Threshold ~ N^{2/3}
    THRESH = (int)cbrt((double)N * N) + 1;
    // More precise: N^{2/3}
    while ((i128)THRESH * THRESH * THRESH < (i128)N * N) THRESH++;

    sieve_mu();

    // D(N) = sum over blocks grouped by q = floor(N/d)
    i128 total = 0;
    i64 d = 1;
    while (d <= N) {
        i64 q = N / d;
        i64 d_max = N / q;
        i64 mu_sum = mertens(d_max) - mertens(d - 1);
        i128 g = (i128)(q + 1) * (q + 1) * (q + 1) - 1;
        total += (i128)mu_sum * g;
        d = d_max + 1;
    }

    // Format: first 9 digits + last 9 digits
    // Convert i128 to string
    string s;
    i128 val = total;
    if (val == 0) {
        s = "0";
    } else {
        bool neg = val < 0;
        if (neg) val = -val;
        while (val > 0) {
            s += '0' + (int)(val % 10);
            val /= 10;
        }
        if (neg) s += '-';
        reverse(s.begin(), s.end());
    }

    string answer = s.substr(0, 9) + s.substr(s.size() - 9);
    cout << answer << endl;
    return 0;
}