// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // IntSequence.hpp // Integer sequences (Fibonacci numbers, Lucas numbers) // Algorithm: square-only fast doubling, O(S(n) log n) // S(n) = squaring cost. Eliminates general multiplication entirely. #pragma once #include "IntBase.hpp" #include "IntOps.hpp" #include "MpnOps.hpp" #include #include #include namespace sangi { /** * @brief Fibonacci and Lucas numbers * * Square-only fast doubling: track the pair (F(k), F(k-1)) and * compute each step with two squarings plus addition/subtraction (no general multiplication). * * Formulas: * F(2k-1) = F(k)^2 + F(k-1)^2 * F(2k+1) = 4*F(k)^2 - F(k-1)^2 + 2*(-1)^k * F(2k) = F(2k+1) - F(2k-1) * * GMP correspondence: mpz_fib_ui, mpz_fib2_ui, mpz_lucnum_ui, mpz_lucnum2_ui */ class IntSequence { public: /** * @brief Fibonacci number F(n) * * F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2) * * @param n Non-negative integer * @return F(n) */ static Int fibonacci(uint64_t n) { return fibonacci2(n).first; } /** * @brief Fibonacci-number pair {F(n), F(n-1)} * * @param n Non-negative integer (when n=0, {F(0), F(-1)} = {0, 1}) * @return {F(n), F(n-1)} */ static std::pair fibonacci2(uint64_t n) { if (n == 0) return { Int(0), Int(1) }; if (n == 1) return { Int(1), Int(0) }; // F(93) is the largest Fibonacci number that fits in uint64_t static constexpr uint64_t fib_table[] = { 0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987, 1597,2584,4181,6765,10946,17711,28657,46368,75025, 121393,196418,317811,514229,832040,1346269,2178309, 3524578,5702887,9227465,14930352,24157817,39088169, 63245986,102334155,165580141,267914296,433494437, 701408733,1134903170,1836311903,2971215073ULL, 4807526976ULL,7778742049ULL,12586269025ULL, 20365011074ULL,32951280099ULL,53316291173ULL, 86267571272ULL,139583862445ULL,225851433717ULL, 365435296162ULL,591286729879ULL,956722026041ULL, 1548008755920ULL,2504730781961ULL,4052739537881ULL, 6557470319842ULL,10610209857723ULL,17167680177565ULL, 27777890035288ULL,44945570212853ULL,72723460248141ULL, 117669030460994ULL,190392490709135ULL,308061521170129ULL, 498454011879264ULL,806515533049393ULL,1304969544928657ULL, 2111485077978050ULL,3416454622906707ULL,5527939700884757ULL, 8944394323791464ULL,14472334024676221ULL,23416728348467685ULL, 37889062373143906ULL,61305790721611591ULL,99194853094755497ULL, 160500643816367088ULL,259695496911122585ULL,420196140727489673ULL, 679891637638612258ULL,1100087778366101931ULL, 1779979416004714189ULL,2880067194370816120ULL, 4660046610375530309ULL,7540113804746346429ULL, 12200160415121876738ULL, }; // Small n: table lookup if (n <= 93) { return { Int(fib_table[n]), Int(fib_table[n > 0 ? n - 1 : 0]) }; } // Use the direct mpn version (GMP-style 3 buffers + table start) for all sizes. // The old 50000 threshold fell back to the IntOps path, but Int construction overhead was large. // Large sizes require heap allocation, but it is still faster than IntOps. // Table start: halve n down to the table range to reduce iteration count // GMP method: nfirst = n >> steps, then double steps times from there uint64_t nfirst = n; int steps = 0; while (nfirst > 93) { nfirst /= 2; steps++; } // Size estimate for F(n): ~0.694 * n bits = ceil(0.694 * n / 64) limbs size_t max_limbs = static_cast(static_cast(n) * 0.694 / 64.0) + 8; if (max_limbs < 8) max_limbs = 8; // 3 buffers (fp, f1p, xp) + scratch // GMP method: fp=F[k], f1p=F[k-1], xp=scratch (no pointer swap needed) size_t alloc = 2 * max_limbs + 4; size_t scratch_sz = mpn::square_scratch_size(max_limbs); size_t total = 3 * alloc + scratch_sz; // Small sizes use a stack buffer (avoids heap allocation and zero initialization) // Large sizes use a thread_local arena to amortize reallocation costs (eliminates the zero-init cost on repeated calls) static constexpr size_t STACK_LIMIT = 4096; // 32KB — fits in the stack up to n ~ 250000 uint64_t stack_mem[STACK_LIMIT]; thread_local std::vector tls_arena; uint64_t* mem_ptr; if (total <= STACK_LIMIT) { mem_ptr = stack_mem; } else { if (tls_arena.size() < total) tls_arena.resize(total); mem_ptr = tls_arena.data(); } uint64_t* fp = mem_ptr; // F[k] uint64_t* f1p = mem_ptr + alloc; // F[k-1] uint64_t* xp = mem_ptr + 2 * alloc; // scratch (holds F[k]^2) uint64_t* scratch = mem_ptr + 3 * alloc; // Set the initial value from the table fp[0] = fib_table[nfirst]; f1p[0] = fib_table[nfirst > 0 ? nfirst - 1 : 0]; size_t size = 1; // GMP-style doubling loop: // xp ← fp² (F[k]²) // fp <- f1p^2 (F[k-1]^2, overwriting fp - safe since the input is f1p) // f1p ← xp + fp (F[2k-1] = F[k]² + F[k-1]²) // fp ← 4*xp - fp ± 2 (F[2k+1] = 4·F[k]² - F[k-1]² + 2·(-1)^k) // bit=1: f1p ← fp - f1p → (fp, f1p) = (F[2k+1], F[2k]) // bit=0: fp ← fp - f1p → (fp, f1p) = (F[2k], F[2k-1]) for (int i = steps - 1; i >= 0; --i) { bool k_odd = (n >> (i + 1)) & 1; // corresponding bit of nfirst = LSB of the current k // Note: since k = n >> i (upper bits), the LSB of k is not the LSB of n >> (i+1), // but the lowest bit above bit (i) of n. As in GMP, test with n & (1 << (i+1)). // Note i+1 cannot reach 64 (n <= 50000). // xp = fp² (F[k]²) mpn::square(xp, fp, size, scratch); // fp = f1p^2 (F[k-1]^2) — safe because fp != f1p mpn::square(fp, f1p, size, scratch); size *= 2; size -= (xp[size - 1] == 0); // f1p = xp + fp (F[2k-1]) f1p[size] = mpn::add(f1p, xp, size, fp, size); // F[2k+1] = 4·F[k]² - F[k-1]² + 2·(-1)^k // GMP bit-OR trick: bit 1 of a square is always 0, so |= 2 absorbs +/-2 if (k_odd) { // (-1)^k = -1 -> -2: OR 2 into fp (= F[k-1]^2) to increase the minuend fp[0] |= 2; } uint64_t cy = mpn::lshift(xp, xp, size, 2); // 4·F[k]² if (!k_odd) { // (-1)^k = +1 -> +2: OR 2 into xp (= 4 * F[k]^2) xp[0] |= 2; } // fp = xp - fp (= 4·F[k]² ± 2 - F[k-1]² ∓ 2 = F[2k+1]) cy -= mpn::sub(fp, xp, size, fp, size); fp[size] = cy; size += (fp[size] != 0); // F[2k] = F[2k+1] - F[2k-1]: replace the one not needed if ((n >> i) & 1) { // bit=1: (fp, f1p) → (F[2k+1], F[2k]) mpn::sub(f1p, fp, size, f1p, size); } else { // bit=0: (fp, f1p) → (F[2k], F[2k-1]) mpn::sub(fp, fp, size, f1p, size); // Since F[2k] < F[2k+1], leading zeros may appear while (size > 0 && fp[size - 1] == 0) --size; } } // Build Int (final result only): fp = F(n), f1p = F(n-1) // Use span + PreNormalized to avoid temporary vector allocation while (size > 0 && fp[size - 1] == 0) --size; size_t f1size = size; while (f1size > 0 && f1p[f1size - 1] == 0) --f1size; Int result_fn, result_fn1; if (size > 0) { result_fn = Int::fromRawWordsPreNormalized( std::span(fp, size), 1); } if (f1size > 0) { result_fn1 = Int::fromRawWordsPreNormalized( std::span(f1p, f1size), 1); } return { std::move(result_fn), std::move(result_fn1) }; } private: // For large sizes: IntOps square-only fast doubling static std::pair fibonacci2_intops(uint64_t n, int bits) { Int a(1), b(0); // F(1), F(0) bool neg = true; Int a2, b2, f2km1, f2kp1, f2k; static const Int TWO(2); for (int i = bits - 2; i >= 0; --i) { // a, b start from Int(1)/Int(0) and are updated only via Unchecked ops -> always Normal IntOps::squareUnchecked(a, a2); IntOps::squareUnchecked(b, b2); // F(2k-1) = a² + b² IntOps::addUnchecked(a2, b2, f2km1); // F(2k+1) = 4*a² - b² + 2*(-1)^k IntOps::leftShiftUnchecked(a2, 2, f2kp1); IntOps::subUnchecked(f2kp1, b2, f2kp1); if (neg) { IntOps::subUnchecked(f2kp1, TWO, f2kp1); } else { IntOps::addUnchecked(f2kp1, TWO, f2kp1); } // F(2k) = F(2k+1) - F(2k-1) IntOps::subUnchecked(f2kp1, f2km1, f2k); if ((n >> i) & 1) { a = std::move(f2kp1); b = std::move(f2k); neg = true; } else { a = std::move(f2k); b = std::move(f2km1); neg = false; } } return { std::move(a), std::move(b) }; } public: /** * @brief Lucas number L(n) * * L(0)=2, L(1)=1, L(n)=L(n-1)+L(n-2) * L(n) = F(n-1) + F(n+1) = 2*F(n+1) - F(n) * * @param n Non-negative integer * @return L(n) */ static Int lucas(uint64_t n) { if (n == 0) return Int(2); auto [fn, fn_1] = fibonacci2(n); // L(n) = F(n) + 2*F(n-1) return fn + fn_1 + fn_1; } /** * @brief Lucas-number pair {L(n), L(n-1)} * * @param n Non-negative integer (when n=0, {L(0), L(-1)} = {2, -1}) * @return {L(n), L(n-1)} */ static std::pair lucas2(uint64_t n) { if (n == 0) return { Int(2), Int(-1) }; auto [fn, fn_1] = fibonacci2(n); // L(n) = F(n) + 2*F(n-1) // L(n-1) = 2*F(n) - F(n-1) Int ln = fn + fn_1 + fn_1; Int ln_1 = fn + fn - fn_1; return { std::move(ln), std::move(ln_1) }; } }; } // namespace sangi