Problem 921: Golden Recurrence

View on Project Euler

Project Euler Problem 921 Solution

EulerSolve provides an optimized solution for Project Euler Problem 921, Golden Recurrence, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary Let \(p=398874989\) and \(G=p^2-1\). The computation is carried out in the two-dimensional algebra $$R=\mathbb{F}_p[u]/(u^2-5),\qquad \beta=u-2,$$ where \(u\) plays the role of \(\sqrt5\). For the Fibonacci numbers $$F_1=F_2=1,\qquad F_n=F_{n-1}+F_{n-2},$$ define the reduced exponent $$e_n\equiv 5^{F_n}\pmod G.$$ If $$\beta^{e_n}=a_n+b_nu,$$ then the \(n\)-th contribution is $$s_n=b_n^5+(-a_n)^5\pmod p.$$ The goal is to evaluate $$S=\sum_{n=2}^{1{,}618{,}034}s_n\pmod p.$$ Directly forming the Fibonacci exponents is hopeless. The successful approach is to keep only a short modular exponent recurrence and to evaluate powers of \(\beta\) inside the fixed algebra \(R\). Mathematical Approach The three implementations all exploit the same structure: the golden-looking base \(\sqrt5-2\), the multiplicative behavior of \(5^{F_n}\), and the fact that every relevant power can be reconstructed from a 64-entry binary table. The split quadratic algebra and the golden element The algebra \(R\) is represented in the basis \(1,u\) with \(u^2=5\). Multiplication is therefore $$\left(a+bu\right)\left(c+du\right)=(ac+5bd)+(ad+bc)u\pmod p.$$ This is exactly the rule used by the implementations. It keeps every power of \(\beta\) in the form \(a+bu\), so the final summand is obtained by reading off two coefficients. The adjective "golden" is not cosmetic....

Detailed mathematical approach

Problem Summary

Let \(p=398874989\) and \(G=p^2-1\). The computation is carried out in the two-dimensional algebra

$$R=\mathbb{F}_p[u]/(u^2-5),\qquad \beta=u-2,$$

where \(u\) plays the role of \(\sqrt5\). For the Fibonacci numbers

$$F_1=F_2=1,\qquad F_n=F_{n-1}+F_{n-2},$$

define the reduced exponent

$$e_n\equiv 5^{F_n}\pmod G.$$

If

$$\beta^{e_n}=a_n+b_nu,$$

then the \(n\)-th contribution is

$$s_n=b_n^5+(-a_n)^5\pmod p.$$

The goal is to evaluate

$$S=\sum_{n=2}^{1{,}618{,}034}s_n\pmod p.$$

Directly forming the Fibonacci exponents is hopeless. The successful approach is to keep only a short modular exponent recurrence and to evaluate powers of \(\beta\) inside the fixed algebra \(R\).

Mathematical Approach

The three implementations all exploit the same structure: the golden-looking base \(\sqrt5-2\), the multiplicative behavior of \(5^{F_n}\), and the fact that every relevant power can be reconstructed from a 64-entry binary table.

The split quadratic algebra and the golden element

The algebra \(R\) is represented in the basis \(1,u\) with \(u^2=5\). Multiplication is therefore

$$\left(a+bu\right)\left(c+du\right)=(ac+5bd)+(ad+bc)u\pmod p.$$

This is exactly the rule used by the implementations. It keeps every power of \(\beta\) in the form \(a+bu\), so the final summand is obtained by reading off two coefficients.

The adjective "golden" is not cosmetic. If

$$\phi=\frac{1+\sqrt5}{2},$$

then \(\phi^3=2+\sqrt5\), hence

$$\beta=\sqrt5-2=\frac{1}{2+\sqrt5}=\phi^{-3}.$$

Also,

$$\left(u-2\right)\left(u+2\right)=u^2-4=1,$$

so \(\beta\) is a unit of the algebra and every positive power is well-defined.

For this specific prime, \(p\equiv 4\pmod 5\). Since \(5\equiv 1\pmod 4\), quadratic reciprocity gives \(\left(\frac{5}{p}\right)=\left(\frac{p}{5}\right)=\left(\frac{4}{5}\right)=1\), so \(u^2-5\) splits over \(\mathbb{F}_p\). In other words, \(R\) is a split quadratic algebra, isomorphic to \(\mathbb{F}_p\times\mathbb{F}_p\). The code does not need that isomorphism explicitly; the coefficient basis \(1,u\) is already enough.

Reducing gigantic exponents to a short modular recurrence

The Fibonacci recurrence immediately gives

$$5^{F_n}=5^{F_{n-1}+F_{n-2}}=5^{F_{n-1}}5^{F_{n-2}}.$$

Because \(R^\times\cong \mathbb{F}_p^\times\times\mathbb{F}_p^\times\), every unit has multiplicative period dividing \(p-1\). So any multiple of \(p-1\) is a safe modulus for exponents. The implementations use the larger quantity

$$G=p^2-1=(p-1)(p+1),$$

which is not minimal but is still perfectly valid because \(p-1\mid G\).

That lets us replace the astronomical integer \(5^{F_n}\) by the residue

$$e_n\equiv 5^{F_n}\pmod G.$$

The residues satisfy the compact recurrence

$$e_1=e_2=5,\qquad e_n\equiv e_{n-1}e_{n-2}\pmod G\quad(n\ge 3).$$

This is the decisive simplification. Instead of carrying Fibonacci numbers or giant exponentials, the algorithm advances the entire exponent side with one modular multiplication per step.

Conjugation and the norm give a permanent invariant

The algebra has an involution \(u\mapsto -u\), so

$$\overline{a+bu}=a-bu.$$

The associated norm is

$$N(a+bu)=(a+bu)(a-bu)=a^2-5b^2\pmod p.$$

For the base element \(\beta=u-2\),

$$N(\beta)=(u-2)(-u-2)=4-u^2=-1.$$

Every \(e_n\) is odd: the sequence starts from \(5,5\), and modulo the even number \(G\) the product of odd residues remains odd. Therefore

$$N\!\left(\beta^{e_n}\right)=N(\beta)^{e_n}=(-1)^{e_n}=-1.$$

So if

$$\beta^{e_n}=a_n+b_nu,$$

then the coefficients always satisfy

$$a_n^2-5b_n^2\equiv -1\pmod p.$$

This Pell-type congruence is not the final answer, but it explains the shape of every coefficient pair and provides a clean mathematical consistency check.

Worked example: the first accumulated terms

Starting from \(\beta=u-2\), repeated multiplication gives

$$\beta^1=-2+u,$$

$$\beta^2=9-4u,$$

$$\beta^3=-38+17u,$$

$$\beta^4=161-72u,$$

$$\beta^5=-682+305u.$$

The tiny checkpoint at exponent \(1\) is

$$s(1)=1^5+2^5=33,$$

because \(\beta^1=-2+u\). The actual sum begins at \(n=2\), where \(e_2=5\), so the first accumulated term is

$$s_2=305^5+682^5\equiv 257933744\pmod p.$$

The next reduced exponent is

$$e_3\equiv e_2e_1\equiv 5\cdot 5\equiv 25\pmod G,$$

and evaluating \(\beta^{25}\) in the same basis gives

$$s_3\equiv 26500067\pmod p.$$

Those two values are exactly the small checkpoints embedded in the implementations. After that, the same recurrence-and-powering pattern continues all the way to \(n=1{,}618{,}034\).

How the Code Works

Representing algebra elements

Each algebra element is stored as a pair of residues \((a,b)\) representing \(a+bu\). Multiplication uses the formula above, so every operation stays in two coordinates modulo \(p\). The quantity \((-a)^5\) is computed modulo \(p\) as the fifth power of the additive inverse of the first coordinate.

Precomputing binary powers of \(\beta\)

The C++, Python, and Java implementations precompute

$$\beta^{2^0},\beta^{2^1},\dots,\beta^{2^{63}}$$

by repeated squaring. That table is sufficient because the chosen exponent modulus satisfies \(0\le e_n<G<2^{64}\). Any required power \(\beta^{e_n}\) is then reconstructed from the binary expansion of \(e_n\).

Streaming the exponent recurrence and the final sum

The implementations never materialize a Fibonacci number. They keep only the two latest exponent residues, both initially equal to \(5\). The answer is initialized with the \(n=2\) term, and then for each \(n\ge 3\) they compute

$$e_n\equiv e_{n-1}e_{n-2}\pmod G,$$

rebuild \(\beta^{e_n}\) from the binary table, extract \(a_n\) and \(b_n\), evaluate \(b_n^5+(-a_n)^5\pmod p\), and add it to the running sum. The three language versions differ only in low-level modular-multiplication details; the mathematical pipeline is identical.

Complexity Analysis

Let \(M=1{,}618{,}034\). Precomputing the binary table costs \(64\) algebra multiplications. Each subsequent term uses one modular multiplication for the exponent recurrence and at most \(64\) algebra multiplications to reconstruct \(\beta^{e_n}\) from its bits. Therefore the running time is

$$O(M\log G),$$

and since \(\log G<64\), this is effectively linear in the number of required indices.

The memory usage is \(O(1)\): a constant-size power table, two current exponents, a running total, and a few temporary algebra elements.

Footnotes and References

  1. Project Euler problem page: https://projecteuler.net/problem=921
  2. Fibonacci number: Wikipedia - Fibonacci number
  3. Golden ratio: Wikipedia - Golden ratio
  4. Modular arithmetic: Wikipedia - Modular arithmetic
  5. Quotient ring: Wikipedia - Quotient ring
  6. Field norm: Wikipedia - Field norm
  7. Quadratic reciprocity: Wikipedia - Quadratic reciprocity
  8. Exponentiation by squaring: Wikipedia - Exponentiation by squaring

Problem 921 source code

C++

#include <array>
#include <cassert>
#include <cstdint>
#include <iostream>

namespace {

using u64 = std::uint64_t;
using u128 = unsigned __int128;

constexpr u64 kMod = 398874989ULL;
constexpr u64 kGroup = kMod * kMod - 1ULL;

struct Elem {
    u64 a;
    u64 b;
};

u64 add_mod(u64 x, u64 y, u64 mod) {
    x += y;
    if (x >= mod) {
        x -= mod;
    }
    return x;
}

u64 mul_mod(u64 x, u64 y, u64 mod) {
    return static_cast<u64>((static_cast<u128>(x) * y) % mod);
}

Elem mul_elem(const Elem& x, const Elem& y) {
    const u64 aa = mul_mod(x.a, y.a, kMod);
    const u64 bb = mul_mod(x.b, y.b, kMod);
    const u64 real = add_mod(aa, (5ULL * bb) % kMod, kMod);
    const u64 imag = add_mod(mul_mod(x.a, y.b, kMod), mul_mod(x.b, y.a, kMod), kMod);
    return {real, imag};
}

std::array<Elem, 64> precompute_powers(const Elem& base) {
    std::array<Elem, 64> powers{};
    powers[0] = base;
    for (int i = 1; i < 64; ++i) {
        powers[i] = mul_elem(powers[i - 1], powers[i - 1]);
    }
    return powers;
}

Elem pow_from_powers(u64 exp, const std::array<Elem, 64>& powers) {
    Elem result{1, 0};
    while (exp != 0) {
        const int bit = __builtin_ctzll(exp);
        result = mul_elem(result, powers[bit]);
        exp &= (exp - 1);
    }
    return result;
}

Elem pow_elem(Elem base, u64 exp) {
    Elem result{1, 0};
    while (exp != 0) {
        if (exp & 1ULL) {
            result = mul_elem(result, base);
        }
        exp >>= 1U;
        if (exp != 0) {
            base = mul_elem(base, base);
        }
    }
    return result;
}

u64 pow_mod(u64 base, u64 exp, u64 mod) {
    u64 result = 1 % mod;
    u64 value = base % mod;
    while (exp != 0) {
        if (exp & 1ULL) {
            result = mul_mod(result, value, mod);
        }
        exp >>= 1U;
        if (exp != 0) {
            value = mul_mod(value, value, mod);
        }
    }
    return result;
}

u64 pow5_mod(u64 x) {
    const u64 x2 = mul_mod(x, x, kMod);
    const u64 x4 = mul_mod(x2, x2, kMod);
    return mul_mod(x4, x, kMod);
}

u64 s_from_exp(u64 exp, const std::array<Elem, 64>& base_powers) {
    const Elem value = pow_from_powers(exp, base_powers);
    const u64 p = value.b;
    const u64 q = (value.a == 0 ? 0 : kMod - value.a);
    return add_mod(pow5_mod(p), pow5_mod(q), kMod);
}

u64 solve() {
    constexpr int kM = 1'618'034;
    const Elem base{(kMod + kMod - 2) % kMod, 1};  // -2 + sqrt(5)
    const auto base_powers = precompute_powers(base);

    u64 exp_prev2 = 5;  // 5^{F_1}
    u64 exp_prev1 = 5;  // 5^{F_2}

    u64 answer = s_from_exp(exp_prev1, base_powers);  // i=2
    for (int i = 3; i <= kM; ++i) {
        const u64 current = mul_mod(exp_prev1, exp_prev2, kGroup);
        answer = add_mod(answer, s_from_exp(current, base_powers), kMod);
        exp_prev2 = exp_prev1;
        exp_prev1 = current;
    }
    return answer;
}

void validate() {
    const Elem base{(kMod + kMod - 2) % kMod, 1};
    const auto base_powers = precompute_powers(base);

    assert(s_from_exp(1, base_powers) == 33);
    assert(s_from_exp(5, base_powers) == 257933744);
    assert(s_from_exp(25, base_powers) == 26500067);

    Elem stepped = base;
    u64 exp = 1;
    for (int n = 0; n <= 20; ++n) {
        const Elem by_exp = pow_from_powers(exp, base_powers);
        assert(by_exp.a == stepped.a && by_exp.b == stepped.b);
        stepped = pow_elem(stepped, 5);
        exp = mul_mod(exp, 5, kGroup);
    }

    u64 fib_prev2 = 1;
    u64 fib_prev1 = 1;
    u64 fexp_prev2 = 5;
    u64 fexp_prev1 = 5;
    for (int i = 3; i <= 25; ++i) {
        const u64 fib_cur = fib_prev1 + fib_prev2;
        const u64 fexp_cur = mul_mod(fexp_prev1, fexp_prev2, kGroup);
        assert(fexp_cur == pow_mod(5, fib_cur, kGroup));
        fib_prev2 = fib_prev1;
        fib_prev1 = fib_cur;
        fexp_prev2 = fexp_prev1;
        fexp_prev1 = fexp_cur;
    }
}

}  // namespace

int main() {
    validate();
    std::cout << solve() << '\n';
    return 0;
}

Python

def solve():
    MOD = 398874989
    GROUP = MOD * MOD - 1
    kM = 1618034

    def add_mod(x, y): return (x + y) % MOD
    def mul_mod(x, y): return x * y % MOD

    def mul_elem(x, y):
        aa = x[0]*y[0]%MOD; bb = x[1]*y[1]%MOD
        real = (aa + 5*bb) % MOD
        imag = (x[0]*y[1] + x[1]*y[0]) % MOD
        return (real, imag)

    def pow_elem(base, exp):
        r = (1, 0)
        while exp > 0:
            if exp & 1: r = mul_elem(r, base)
            exp >>= 1
            if exp > 0: base = mul_elem(base, base)
        return r

    def pow5(x):
        x2 = x*x%MOD; x4 = x2*x2%MOD; return x4*x%MOD

    base = ((MOD + MOD - 2) % MOD, 1)
    # Precompute powers of base for fast exponentiation
    powers = [base]
    for _ in range(63): powers.append(mul_elem(powers[-1], powers[-1]))

    def pow_base(exp):
        r = (1, 0)
        while exp:
            b = (exp & -exp).bit_length() - 1
            r = mul_elem(r, powers[b]); exp &= exp - 1
        return r

    def s_from_exp(exp):
        v = pow_base(exp)
        p = v[1]; q = (MOD - v[0]) % MOD
        return (pow5(p) + pow5(q)) % MOD

    ep2 = 5; ep1 = 5  # 5^{F_1}, 5^{F_2}
    answer = s_from_exp(ep1)
    for i in range(3, kM + 1):
        cur = ep1 * ep2 % GROUP
        answer = (answer + s_from_exp(cur)) % MOD
        ep2, ep1 = ep1, cur

    return str(answer)

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

Java

public class Euler921 {

    static final long MOD = 398874989L;
    static final long GROUP = MOD * MOD - 1L;

    static class Elem {
        long a, b;

        Elem(long a, long b) {
            this.a = a;
            this.b = b;
        }
    }

    static long addMod(long x, long y, long mod) {
        long res = x + y;
        if (res >= mod)
            res -= mod;
        return res;
    }

    static long mulMod(long x, long y, long mod) {
        long res = 0;
        long a = x;
        long b = y;
        while (b > 0) {
            if ((b & 1L) != 0) {
                res = addMod(res, a, mod);
            }
            a = addMod(a, a, mod);
            b >>= 1L;
        }
        return res;
    }

    static Elem mulElem(Elem x, Elem y) {
        long aa = mulMod(x.a, y.a, MOD);
        long bb = mulMod(x.b, y.b, MOD);
        long real = addMod(aa, mulMod(5L, bb, MOD), MOD);
        long imag = addMod(mulMod(x.a, y.b, MOD), mulMod(x.b, y.a, MOD), MOD);
        return new Elem(real, imag);
    }

    static Elem[] precomputePowers(Elem base) {
        Elem[] powers = new Elem[64];
        powers[0] = base;
        for (int i = 1; i < 64; ++i) {
            powers[i] = mulElem(powers[i - 1], powers[i - 1]);
        }
        return powers;
    }

    static Elem powFromPowers(long exp, Elem[] powers) {
        Elem result = new Elem(1, 0);
        int bit = 0;
        while (exp != 0) {
            if ((exp & 1L) != 0) {
                result = mulElem(result, powers[bit]);
            }
            exp >>= 1L;
            bit++;
        }
        return result;
    }

    static long pow5Mod(long x) {
        long x2 = mulMod(x, x, MOD);
        long x4 = mulMod(x2, x2, MOD);
        return mulMod(x4, x, MOD);
    }

    static long sFromExp(long exp, Elem[] powers) {
        Elem value = powFromPowers(exp, powers);
        long p = value.b;
        long q = value.a == 0 ? 0 : MOD - value.a;
        return addMod(pow5Mod(p), pow5Mod(q), MOD);
    }

    public static String solve() {
        int kM = 1618034;
        Elem base = new Elem((MOD + MOD - 2) % MOD, 1);
        Elem[] basePowers = precomputePowers(base);

        long expPrev2 = 5;
        long expPrev1 = 5;

        long answer = sFromExp(expPrev1, basePowers);
        for (int i = 3; i <= kM; ++i) {
            long current = mulMod(expPrev1, expPrev2, GROUP);
            answer = addMod(answer, sFromExp(current, basePowers), MOD);
            expPrev2 = expPrev1;
            expPrev1 = current;
        }

        return Long.toString(answer);
    }

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