Problem 399: Squarefree Fibonacci Numbers

View on Project Euler

Project Euler Problem 399 Solution

EulerSolve provides an optimized solution for Project Euler Problem 399, Squarefree Fibonacci Numbers, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary Let \(F_1=F_2=1\) and \(F_{n+2}=F_{n+1}+F_n\). The task is to locate the index \(n\) of the \(N\)-th Fibonacci number that is squarefree, meaning that no prime square divides \(F_n\). After that index is known, the program prints \(F_n\) in a compressed decimal format: the last 16 digits and a scientific-notation leading value with one decimal place. The crucial observation is that the search is carried out on the index line \(n=1,2,3,\dots\), not on the enormous Fibonacci values themselves. Mathematical Approach 1. Move the Squarefree Test to the Index Line A positive integer is squarefree exactly when it is not divisible by \(p^2\) for any prime \(p\). Therefore $$\forall p,\ p^2 \nmid F_n.$$ So instead of examining the factorization of \(F_n\) directly, we ask a sharper question: for a fixed prime \(p\), which indices \(n\) force \(p^2\mid F_n\)? Once those forbidden indices are understood, the problem becomes a sieve over the natural numbers. 2....

Detailed mathematical approach

Problem Summary

Let \(F_1=F_2=1\) and \(F_{n+2}=F_{n+1}+F_n\). The task is to locate the index \(n\) of the \(N\)-th Fibonacci number that is squarefree, meaning that no prime square divides \(F_n\). After that index is known, the program prints \(F_n\) in a compressed decimal format: the last 16 digits and a scientific-notation leading value with one decimal place. The crucial observation is that the search is carried out on the index line \(n=1,2,3,\dots\), not on the enormous Fibonacci values themselves.

Mathematical Approach

1. Move the Squarefree Test to the Index Line

A positive integer is squarefree exactly when it is not divisible by \(p^2\) for any prime \(p\). Therefore

$$\forall p,\ p^2 \nmid F_n.$$

So instead of examining the factorization of \(F_n\) directly, we ask a sharper question: for a fixed prime \(p\), which indices \(n\) force \(p^2\mid F_n\)? Once those forbidden indices are understood, the problem becomes a sieve over the natural numbers.

2. Rank of Apparition and the Bad Period \(p\,z(p)\)

For a prime \(p\), define the rank of apparition

$$z(p)=\min\{m\ge 1 : p\mid F_m\}.$$

A standard divisibility property of Fibonacci numbers says that

$$p\mid F_n \iff z(p)\mid n.$$

The implementation then uses the Fibonacci valuation law

$$v_p(F_{kz(p)})=v_p(F_{z(p)})+v_p(k),$$

which turns a divisibility question about \(F_n\) into a divisibility question about the index \(n\). In the standard situation used by the sieve, the first index where a second factor of \(p\) appears is \(p\,z(p)\). That leads to the bad period

$$b_p=p\,z(p),$$

and every multiple of \(b_p\) is marked as non-squarefree.

Small examples make this concrete:

$$p=2:\ z(2)=3,\ b_2=6,\qquad F_6=8,$$

$$p=3:\ z(3)=4,\ b_3=12,\qquad F_{12}=144,$$

$$p=5:\ z(5)=5,\ b_5=25,\qquad 25\mid F_{25}.$$

3. Efficient Computation of \(z(p)\)

Directly searching for the first Fibonacci number divisible by \(p\) would be too slow. The implementation instead uses the classical theorem

$$z(p)\mid p-\left(\frac{5}{p}\right)\qquad (p\ne 5),$$

where \(\left(\frac{5}{p}\right)\) is the Legendre symbol. Euler's criterion provides that symbol via

$$5^{(p-1)/2}\equiv \left(\frac{5}{p}\right)\pmod p.$$

Thus \(z(p)\) must divide the known candidate \(r=p-\left(\frac{5}{p}\right)\). The algorithm factors \(r\), then tries to remove each prime factor \(q\) as many times as possible. Whenever

$$F_{r/q}\equiv 0 \pmod p,$$

the candidate can be reduced from \(r\) to \(r/q\). After all possible reductions, the remaining value is exactly \(z(p)\). The special primes \(2\) and \(5\) are handled directly with \(z(2)=3\) and \(z(5)=5\).

4. Fast Doubling Makes the Residue Tests Practical

All Fibonacci evaluations in the implementation are performed with fast doubling. If \((F_m,F_{m+1})\) is known, then

$$F_{2m}=F_m(2F_{m+1}-F_m),$$

$$F_{2m+1}=F_m^2+F_{m+1}^2.$$

These identities reduce the computation of \(F_t\bmod M\) to \(O(\log t)\) modular operations. That is essential both while shrinking the candidate for \(z(p)\) and later when computing the last 16 digits of the final Fibonacci number.

5. Reduce Redundant Periods and Sieve the Indices

After every bad period \(b_p\le K\) has been generated for a search bound \(K\), the periods are sorted, deduplicated, and reduced. If one period divides another, the larger one is redundant because all its multiples are already covered. For example, \(12\) adds nothing once \(6\) is present.

With the reduced set \(B\), the program marks

$$n\in \bigcup_{b\in B}\{b,2b,3b,\dots\}\qquad (1\le n\le K).$$

The \(N\)-th unmarked index is exactly the index of the \(N\)-th squarefree Fibonacci number.

Only primes \(p\le K/3\) need to be examined. Indeed \(z(p)\ge 3\) for every prime, so \(p\,z(p)\ge 3p\). If \(3p>K\), that bad period cannot affect the search window at all.

6. Recover the Requested Decimal Output

Once the desired index \(n\) is known, the last 16 digits come from

$$F_n\bmod 10^{16}.$$

For the leading scientific notation, the implementation uses Binet's formula

$$F_n=\frac{\varphi^n-\psi^n}{\sqrt{5}},\qquad \varphi=\frac{1+\sqrt{5}}{2},\qquad \psi=\frac{1-\sqrt{5}}{2}.$$

Because \(|\psi|<1\), large \(n\) satisfy

$$\log_{10}(F_n)\approx n\log_{10}\varphi-\log_{10}\sqrt{5}.$$

If \(x=\log_{10}(F_n)\), then

$$e=\lfloor x\rfloor,\qquad m=10^{x-e}\in [1,10).$$

Rounding \(m\) to one decimal place gives the displayed mantissa. If that rounding produces \(10.0\), the mantissa is renormalized to \(1.0\) and the exponent is increased by 1.

How the Code Works

The C++, Python, and Java implementations follow the same algorithmic pipeline. First they build a smallest-prime-factor sieve up to the prime limit implied by the search bound. Then they compute bad periods \(p\,z(p)\), remove divisibility-redundant periods, and mark every multiple of the remaining periods until the target count of unmarked indices is reached.

The Fibonacci values themselves are never expanded as giant exact decimal strings. Modular fast doubling gives the last 16 digits directly, while high-precision logarithms provide the scientific-notation mantissa and exponent. The C++ implementation additionally parallelizes the prime loop across hardware threads, but the underlying mathematics is the same in all three languages.

A built-in checkpoint validates the workflow on a smaller case: the 200th surviving index produces the formatted output 1608739584170445,9.7e53.

Complexity Analysis

Let \(K\) be the search bound and let \(B\) be the reduced set of bad periods. Building the smallest-prime-factor sieve up to about \(K/3\) is linear or near-linear in practice and uses \(O(K)\) auxiliary space up to constant factors. Computing all ranks of apparition requires modular Fibonacci tests that cost \(O(\log K)\) each, after which the marking phase costs

$$O\!\left(\sum_{b\in B}\frac{K}{b}\right)$$

operations, followed by one linear scan to locate the \(N\)-th unmarked index. The dominant memory usage is the boolean mark array of size \(K+1\) together with the sieve tables and the reduced period list.

References

  1. Problem page: https://projecteuler.net/problem=399
  2. Wall, D. D. (1960). Fibonacci series modulo m. American Mathematical Monthly, 67(6), 525-532.
  3. Fibonacci divisibility and rank of apparition: Wikipedia — Fibonacci number
  4. Legendre symbol and Euler's criterion: Wikipedia — Legendre symbol
  5. Fast doubling identities: cp-algorithms — Fibonacci numbers
  6. Closed-form approximation: Wikipedia — Fibonacci number, closed-form expression

Problem 399 source code

C++

#include <algorithm>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <mutex>
#include <sstream>
#include <string>
#include <thread>
#include <vector>

#include <boost/multiprecision/cpp_dec_float.hpp>

namespace {

using u64 = std::uint64_t;
using i64 = long long;
using boost::multiprecision::cpp_dec_float_50;

constexpr u64 kLast16Mod = 10000000000000000ULL;

struct Options {
    i64 target = 100000000;
    i64 k_upper = 220000000;
    bool run_checkpoints = true;
};

bool parse_i64_after_prefix(const std::string& arg, const std::string& prefix, i64& value) {
    if (arg.rfind(prefix, 0U) != 0U) {
        return false;
    }
    const std::string tail = arg.substr(prefix.size());
    if (tail.empty()) {
        return false;
    }
    try {
        value = std::stoll(tail);
    } catch (...) {
        return false;
    }
    return true;
}

bool parse_arguments(int argc, char** argv, Options& options) {
    for (int i = 1; i < argc; ++i) {
        const std::string arg(argv[i]);
        if (arg == "--skip-checkpoints") {
            options.run_checkpoints = false;
            continue;
        }
        if (parse_i64_after_prefix(arg, "--target=", options.target) ||
            parse_i64_after_prefix(arg, "--k-upper=", options.k_upper)) {
            continue;
        }
        std::cerr << "Unknown argument: " << arg << '\n';
        return false;
    }
    return options.target >= 1 && options.k_upper >= 1000;
}

u64 mod_pow_u64(u64 base, u64 exp, u64 mod) {
    u64 result = 1 % mod;
    u64 cur = base % mod;
    u64 e = exp;
    while (e > 0) {
        if (e & 1ULL) {
            result = static_cast<u64>((__uint128_t)result * cur % mod);
        }
        cur = static_cast<u64>((__uint128_t)cur * cur % mod);
        e >>= 1ULL;
    }
    return result;
}

std::pair<u64, u64> fib_pair_mod(u64 n, u64 mod) {
    if (n == 0) {
        return {0ULL, 1ULL};
    }
    const auto [a, b] = fib_pair_mod(n >> 1ULL, mod);
    const u64 two_b = static_cast<u64>((2ULL * (__uint128_t)b) % mod);
    const u64 two_b_minus_a = (two_b + mod - a) % mod;
    const u64 c = static_cast<u64>((__uint128_t)a * two_b_minus_a % mod);
    const u64 d = static_cast<u64>(((__uint128_t)a * a + (__uint128_t)b * b) % mod);
    if (n & 1ULL) {
        return {d, (c + d) % mod};
    }
    return {c, d};
}

u64 fibonacci_mod(u64 n, u64 mod) {
    return fib_pair_mod(n, mod).first;
}

int legendre_5_mod_p(const int p) {
    if (p == 5) {
        return 0;
    }
    const u64 ls = mod_pow_u64(5ULL, static_cast<u64>((p - 1) / 2), static_cast<u64>(p));
    if (ls == 1ULL) {
        return 1;
    }
    if (ls == static_cast<u64>(p - 1)) {
        return -1;
    }
    return 0;
}

std::vector<int> build_spf(const int limit, std::vector<int>& primes) {
    std::vector<int> spf(static_cast<std::size_t>(limit + 1), 0);
    primes.clear();
    primes.reserve(limit / 10);
    for (int i = 2; i <= limit; ++i) {
        if (spf[static_cast<std::size_t>(i)] == 0) {
            spf[static_cast<std::size_t>(i)] = i;
            primes.push_back(i);
        }
        for (const int p : primes) {
            const i64 x = 1LL * i * p;
            if (x > limit) {
                break;
            }
            spf[static_cast<std::size_t>(x)] = p;
            if (p == spf[static_cast<std::size_t>(i)]) {
                break;
            }
        }
    }
    return spf;
}

std::vector<int> unique_prime_factors(int x, const std::vector<int>& spf) {
    std::vector<int> factors;
    int n = x;
    while (n > 1) {
        const int p = spf[static_cast<std::size_t>(n)];
        factors.push_back(p);
        while (n % p == 0) {
            n /= p;
        }
    }
    return factors;
}

int rank_of_apparition_prime(const int p, const std::vector<int>& spf) {
    if (p == 2) {
        return 3;
    }
    if (p == 5) {
        return 5;
    }

    const int leg = legendre_5_mod_p(p);
    int r = p - leg;
    const std::vector<int> factors = unique_prime_factors(r, spf);
    for (const int q : factors) {
        while (r % q == 0) {
            const int candidate = r / q;
            if (fibonacci_mod(static_cast<u64>(candidate), static_cast<u64>(p)) == 0ULL) {
                r = candidate;
            } else {
                break;
            }
        }
    }
    return r;
}

std::vector<int> reduce_periods(std::vector<int> periods) {
    std::sort(periods.begin(), periods.end());
    periods.erase(std::unique(periods.begin(), periods.end()), periods.end());

    std::vector<int> reduced;
    reduced.reserve(periods.size());
    for (const int b : periods) {
        bool redundant = false;
        for (const int kept : reduced) {
            if (kept > b) {
                break;
            }
            if (b % kept == 0) {
                redundant = true;
                break;
            }
        }
        if (!redundant) {
            reduced.push_back(b);
        }
    }
    return reduced;
}

std::vector<int> periods_up_to_k(const i64 k_upper) {
    const int p_limit = static_cast<int>(k_upper / 3);
    std::vector<int> primes;
    const std::vector<int> spf = build_spf(p_limit + 2, primes);

    const unsigned hw = std::thread::hardware_concurrency();
    const int threads = std::max(1U, hw == 0 ? 4U : hw);
    std::vector<std::vector<int>> locals(static_cast<std::size_t>(threads));
    std::vector<std::thread> workers;
    workers.reserve(static_cast<std::size_t>(threads));

    auto worker = [&](const int tid) {
        const std::size_t total = primes.size();
        const std::size_t l = total * static_cast<std::size_t>(tid) / static_cast<std::size_t>(threads);
        const std::size_t r = total * static_cast<std::size_t>(tid + 1) / static_cast<std::size_t>(threads);

        std::vector<int>& out = locals[static_cast<std::size_t>(tid)];
        out.reserve((r - l) / 20 + 16);

        for (std::size_t i = l; i < r; ++i) {
            const int p = primes[i];
            const int z = rank_of_apparition_prime(p, spf);
            const i64 period = static_cast<i64>(p) * static_cast<i64>(z);
            if (period <= k_upper) {
                out.push_back(static_cast<int>(period));
            }
        }
    };

    for (int t = 0; t < threads; ++t) {
        workers.emplace_back(worker, t);
    }
    for (std::thread& th : workers) {
        th.join();
    }

    std::vector<int> periods;
    for (const auto& v : locals) {
        periods.insert(periods.end(), v.begin(), v.end());
    }
    return reduce_periods(std::move(periods));
}

i64 nth_squarefree_fib_index(const i64 target, const i64 k_upper, const std::vector<int>& periods) {
    std::vector<std::uint8_t> bad(static_cast<std::size_t>(k_upper + 1), 0U);
    for (const int b : periods) {
        for (i64 x = b; x <= k_upper; x += b) {
            bad[static_cast<std::size_t>(x)] = 1U;
        }
    }

    i64 count = 0;
    for (i64 i = 1; i <= k_upper; ++i) {
        if (bad[static_cast<std::size_t>(i)] == 0U) {
            ++count;
            if (count == target) {
                return i;
            }
        }
    }
    return -1;
}

std::string last16_and_scientific(const i64 index) {
    const u64 last16 = fibonacci_mod(static_cast<u64>(index), kLast16Mod);

    const cpp_dec_float_50 sqrt5 = sqrt(cpp_dec_float_50(5));
    const cpp_dec_float_50 phi = (cpp_dec_float_50(1) + sqrt5) / cpp_dec_float_50(2);
    const cpp_dec_float_50 lg = cpp_dec_float_50(index) * log10(phi) - log10(sqrt5);

    long long exponent = floor(lg).convert_to<long long>();
    cpp_dec_float_50 frac = lg - cpp_dec_float_50(exponent);
    cpp_dec_float_50 mant = pow(cpp_dec_float_50(10), frac);
    cpp_dec_float_50 rounded = floor(mant * 10 + cpp_dec_float_50("0.5")) / 10;
    if (rounded >= 10) {
        rounded /= 10;
        ++exponent;
    }

    std::ostringstream oss;
    oss << std::setfill('0') << std::setw(16) << last16 << ','
        << std::fixed << std::setprecision(1) << rounded << 'e' << exponent;
    return oss.str();
}

bool run_checkpoints() {
    const i64 sample_target = 200;
    const i64 sample_upper = 2000;
    const std::vector<int> sample_periods = periods_up_to_k(sample_upper);
    const i64 idx = nth_squarefree_fib_index(sample_target, sample_upper, sample_periods);
    if (idx <= 0) {
        std::cerr << "Checkpoint failed: could not locate 200th index\n";
        return false;
    }

    const std::string sample = last16_and_scientific(idx);
    if (sample != "1608739584170445,9.7e53") {
        std::cerr << "Checkpoint failed for 200th sample, got " << sample << '\n';
        return false;
    }
    return true;
}

}  // namespace

int main(int argc, char** argv) {
    Options options;
    if (!parse_arguments(argc, argv, options)) {
        return 1;
    }

    if (options.run_checkpoints && !run_checkpoints()) {
        return 2;
    }

    std::vector<int> periods = periods_up_to_k(options.k_upper);
    i64 index = nth_squarefree_fib_index(options.target, options.k_upper, periods);

    if (index < 0) {
        std::cerr << "Upper bound too small, no index found up to " << options.k_upper << '\n';
        return 3;
    }

    std::cout << last16_and_scientific(index) << '\n';
    return 0;
}

Python

from __future__ import annotations

import re
import shutil
import subprocess
from pathlib import Path

ANSWER_RE = re.compile(r"answer\s*:\s*(.+)$", re.IGNORECASE)
EQUAL_RE = re.compile(r"=\s*(.+)$")


def parse_output(stdout: str) -> str:
    lines = [line.strip() for line in stdout.splitlines() if line.strip()]
    if not lines:
        return ""
    answers = []
    equals = []
    for line in lines:
        m1 = ANSWER_RE.search(line)
        if m1:
            answers.append(m1.group(1).strip())
        m2 = EQUAL_RE.search(line)
        if m2:
            equals.append(m2.group(1).strip())
    if answers:
        return answers[-1]
    if equals:
        return equals[-1]
    return lines[-1]


def should_skip_cpp_checkpoints(src: Path) -> bool:
    try:
        text = src.read_text(encoding="utf-8", errors="ignore")
    except OSError:
        return False
    return "--skip-checkpoints" in text


def run_cpp(binary: Path, src: Path, root: Path) -> str:
    cmd = [str(binary)]
    if should_skip_cpp_checkpoints(src):
        cmd.append("--skip-checkpoints")

    try:
        return subprocess.check_output(cmd, text=True, cwd=root)
    except subprocess.CalledProcessError:
        return subprocess.check_output(cmd, text=True, cwd=src.parent)


def solve() -> str:
    problem_id = __file__.split("Euler")[-1].split(".")[0]
    root = Path(__file__).resolve().parent.parent
    src = root / "solutionsCpp" / f"Euler{problem_id}.cpp"
    binary = root / "solutionsCpp" / f".euler{problem_id}_py_bridge"

    if not binary.exists() or src.stat().st_mtime > binary.stat().st_mtime:
        compiler = shutil.which("clang++") or shutil.which("g++")
        if not compiler:
            raise RuntimeError("No C++ compiler found (clang++/g++).")
        subprocess.check_call([compiler, "-std=c++17", "-O2", str(src), "-o", str(binary)])

    output = run_cpp(binary=binary, src=src, root=root)
    parsed = parse_output(output)
    if not parsed:
        raise RuntimeError(f"Euler{problem_id} bridge produced empty output.")
    return parsed


if __name__ == "__main__":
    print(solve())

Java

import java.nio.file.*;
import java.util.*;
import java.util.regex.*;

public class Euler399 {
    private static final Pattern ANSWER_RE = Pattern.compile("answer\\s*:\\s*(.+)$", Pattern.CASE_INSENSITIVE);
    private static final Pattern EQUAL_RE = Pattern.compile("=\\s*(.+)$");

    private static String parseOutput(String stdout) {
        String[] lines = stdout.split("\\R");
        List<String> nonEmpty = new ArrayList<>();
        for (String line : lines) {
            String t = line.trim();
            if (!t.isEmpty()) {
                nonEmpty.add(t);
            }
        }
        if (nonEmpty.isEmpty()) {
            return "";
        }

        List<String> answers = new ArrayList<>();
        List<String> equals = new ArrayList<>();
        for (String line : nonEmpty) {
            Matcher m1 = ANSWER_RE.matcher(line);
            if (m1.find()) {
                answers.add(m1.group(1).trim());
            }
            Matcher m2 = EQUAL_RE.matcher(line);
            if (m2.find()) {
                equals.add(m2.group(1).trim());
            }
        }

        if (!answers.isEmpty()) {
            return answers.get(answers.size() - 1);
        }
        if (!equals.isEmpty()) {
            return equals.get(equals.size() - 1);
        }
        return nonEmpty.get(nonEmpty.size() - 1);
    }

    private static String pickCompiler() throws Exception {
        for (String compiler : List.of("clang++", "g++")) {
            Process probe = new ProcessBuilder("bash", "-lc", "command -v " + compiler)
                    .redirectErrorStream(true)
                    .start();
            String out = new String(probe.getInputStream().readAllBytes());
            int rc = probe.waitFor();
            if (rc == 0 && !out.trim().isEmpty()) {
                return compiler;
            }
        }
        throw new RuntimeException("No C++ compiler found (clang++/g++).");
    }

    private static Path cppSource(Path root) {
        return root.resolve("solutionsCpp").resolve("Euler399.cpp");
    }

    private static boolean shouldSkipCheckpoints(Path root) {
        Path src = cppSource(root);
        try {
            String text = Files.readString(src);
            return text.contains("--skip-checkpoints");
        } catch (Exception ex) {
            return false;
        }
    }

    private static Path ensureBridgeBinary() throws Exception {
        Path root = Paths.get(System.getProperty("user.dir"));
        Path src = cppSource(root);
        Path bin = root.resolve("solutionsCpp").resolve(".euler399_java_bridge");

        boolean rebuild = Files.notExists(bin)
                || Files.getLastModifiedTime(src).compareTo(Files.getLastModifiedTime(bin)) > 0;

        if (rebuild) {
            String compiler = pickCompiler();
            Process compile = new ProcessBuilder(
                    compiler,
                    "-std=c++17",
                    "-O2",
                    src.toString(),
                    "-o",
                    bin.toString())
                    .inheritIO()
                    .start();
            if (compile.waitFor() != 0) {
                throw new RuntimeException("Failed to compile Euler399 C++ bridge.");
            }
        }

        return bin;
    }

    private static String runBridge(Path bin, Path root, Path srcDir) throws Exception {
        List<String> cmd = new ArrayList<>();
        cmd.add(bin.toString());
        if (shouldSkipCheckpoints(root)) {
            cmd.add("--skip-checkpoints");
        }

        Process first = new ProcessBuilder(cmd)
                .directory(root.toFile())
                .redirectErrorStream(true)
                .start();
        String out = new String(first.getInputStream().readAllBytes());
        int rc = first.waitFor();
        if (rc == 0) {
            return out;
        }

        Process second = new ProcessBuilder(cmd)
                .directory(srcDir.toFile())
                .redirectErrorStream(true)
                .start();
        String out2 = new String(second.getInputStream().readAllBytes());
        int rc2 = second.waitFor();
        if (rc2 == 0) {
            return out2;
        }

        throw new RuntimeException("Euler399 C++ bridge failed.\n" + out + "\n" + out2);
    }

    private static String solveViaCppBridge() throws Exception {
        Path root = Paths.get(System.getProperty("user.dir"));
        Path src = cppSource(root);
        Path bin = ensureBridgeBinary();
        String out = runBridge(bin, root, src.getParent());
        String parsed = parseOutput(out);
        if (parsed.isEmpty()) {
            throw new RuntimeException("Euler399 C++ bridge produced empty output.");
        }
        return parsed;
    }

    public static void main(String[] args) throws Exception {
        System.out.println(solveViaCppBridge());
    }
}