// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // IntNumberTheory.hpp // Number-theoretic functions (divisor enumeration, Möbius function, Euler's totient, Jacobi symbol) #pragma once #include "IntBase.hpp" #include "IntFactorTable.hpp" #include "IntGCD.hpp" #include "MpnOps.hpp" #include #include #include namespace sangi { /** * @brief Number-theoretic functions * * Provides number-theoretic functions based on prime factorization. * Uses IntFactorTable to perform prime factorization, then computes * divisor enumeration, the Möbius function, and Euler's totient. */ class IntNumberTheory { public: /** * @brief Returns all positive divisors of n in ascending order * * @param n Positive integer (returns an empty vector when n <= 0) * @param table Prime-factor table * @return Vector of divisors (sorted ascending) * * Algorithm: * From the prime factorization n = p1^e1 * p2^e2 * ... * pk^ek, * recursively generate all combinations of prime powers 0..ei. * * Example: * divisors(12, table) → {1, 2, 3, 4, 6, 12} * divisors(1, table) → {1} * divisors(0, table) → {} */ static std::vector divisors(const Int& n, const IntFactorTable& table) { if (n <= 0) return {}; auto factors = table.factorize(n); if (factors.empty()) return { Int(1) }; // n == 1 // Compute the number of divisors size_t count = 1; for (auto& [p, e] : factors) count *= static_cast(e + 1); // Generate divisors: every combination of prime-power exponents std::vector result; result.reserve(count); result.push_back(Int(1)); for (auto& [p, e] : factors) { size_t prev_size = result.size(); Int pk(1); for (int k = 1; k <= e; ++k) { pk *= p; for (size_t j = 0; j < prev_size; ++j) result.push_back(result[j] * pk); } } std::sort(result.begin(), result.end()); return result; } /** * @brief Möbius function mu(n) * * @param n Positive integer (n <= 0 -> 0) * @param table Prime-factor table * @return μ(n): * - μ(1) = 1 * - mu(n) = 0 if n has a squared prime factor * - mu(n) = (-1)^k if n = p1*p2*...*pk (product of distinct primes) * * Reference: Serizawa Masami, "Introduction to Primes", p.206 */ static int moebiusMu(const Int& n, const IntFactorTable& table) { if (n <= 0) return 0; if (n.isOne()) return 1; auto factors = table.factorize(n); // Check whether n has a squared prime factor for (auto& [p, e] : factors) { if (e >= 2) return 0; } // +1 if the number of distinct prime factors is even, -1 if odd return (factors.size() & 1) ? -1 : 1; } /** * @brief Euler's totient phi(n) * * Returns the count of positive integers <= n that are coprime to n. * * @param n Positive integer (n <= 0 -> 0) * @param table Prime-factor table * @return φ(n) = n * Π_{p|n} (1 - 1/p) * * Algorithm: * φ(n) = n * Π (1 - 1/p_i) = n - Σ n/p_i + ... * Implemented in integer arithmetic: result = n; for each prime factor p, result -= result / p * * Reference: Serizawa Masami, "Introduction to Primes", p.200 */ static Int eulerPhi(const Int& n, const IntFactorTable& table) { if (n <= 0) return Int(0); if (n.isOne()) return Int(1); auto factors = table.factorize(n); Int result = n; for (auto& [p, e] : factors) result -= result / p; return result; } /** * @brief Number of divisors sigma_0(n) * * @param n Positive integer * @param table Prime-factor table * @return Number of divisors of n */ static Int divisorCount(const Int& n, const IntFactorTable& table) { if (n <= 0) return Int(0); if (n.isOne()) return Int(1); auto factors = table.factorize(n); Int count(1); for (auto& [p, e] : factors) count *= Int(e + 1); return count; } /** * @brief Sum of divisors sigma_1(n) * * @param n Positive integer * @param table Prime-factor table * @return Sum of all divisors of n * * Algorithm: * σ(n) = Π_{p^e || n} (p^{e+1} - 1) / (p - 1) */ static Int divisorSum(const Int& n, const IntFactorTable& table) { if (n <= 0) return Int(0); if (n.isOne()) return Int(1); auto factors = table.factorize(n); Int result(1); for (auto& [p, e] : factors) { // (p^{e+1} - 1) / (p - 1) = 1 + p + p^2 + ... + p^e Int sum(1), pk(1); for (int k = 1; k <= e; ++k) { pk *= p; sum += pk; } result *= sum; } return result; } // ===================================================================== // Jacobi symbol / Legendre symbol / Kronecker symbol // ===================================================================== /** * @brief Jacobi symbol (a/n) * * When n is a positive odd integer, compute the Jacobi symbol (a/n). * Coincides with the Legendre symbol when n is prime. * * Algorithm: iteration based on quadratic reciprocity * 1. Reduce a to a mod n * 2. If a is even, factor out 2 and compute (2/n) * 3. Apply quadratic reciprocity to invert (a/n) = +/-(n/a) * 4. Swap a and n and repeat * * @param a Arbitrary integer * @param n Positive odd integer (n > 0 and n odd) * @return One of -1, 0, 1 * * Special cases: * jacobi(0, n) = 0 (n > 1), jacobi(0, 1) = 1 * jacobi(1, n) = 1 * n even or n <= 0 -> 0 (undefined) */ // uint64-based Jacobi helper: standard binary Jacobi // Precondition: n is a positive odd integer (n & 1 == 1); result is the accumulated sign (+/-1) static int jacobiSymbol_u64(uint64_t a, uint64_t n, int result) { while (a != 0) { while ((a & 1) == 0) { a >>= 1; int n8 = static_cast(n & 7); if (n8 == 3 || n8 == 5) result = -result; } if ((a & 3) == 3 && (n & 3) == 3) result = -result; uint64_t tmp = a; a = n % a; n = tmp; if (n == 1) return result; } return (n == 1) ? result : 0; } // Convert the bits state to a +/-1 accumulated sign and pass to the 1-limb u64 fast path // bits encoding (from gmp-impl.h): // bit 0: e (sign, 0=+1, 1=-1) // bits 1-4: state index -> decodes (a mod 4, b mod 4, d) // When bits >= 16, d=0 (a is denominator): swap a, b to canonical (a/b) form static int jacobiSymbol_finish_u64(uint64_t a, uint64_t b, unsigned bits) { if (bits == mpn::BITS_FAIL) return 0; int sign = 1 - 2 * static_cast(bits & 1); if (bits >= 16) std::swap(a, b); // At this point b should be odd (the denominator) if (b == 0) return (a == 1) ? sign : 0; if (b == 1) return sign; if (a == 0) return (b == 1) ? sign : 0; return jacobiSymbol_u64(a, b, sign); } // mpn-level Jacobi loop (modeled on GMP's mpn_jacobi_n) // ap, np are mutable buffers (allocated by caller, padded to a common width) // bits is the state initialized by jacobi_init // Return value: +/-1 / 0 // // Convention: ap = "a" (numerator, GMP's a), np = "b" (denominator, GMP's b) // Invariant: update bits at each Euclid step (within subdiv or hgcd2_jacobi) // The state machine handles 2-stripping automatically (Schönhage's rule via table) static int jacobiSymbol_mpn_loop(uint64_t* ap, size_t an, uint64_t* np, size_t nnn, unsigned bits) { thread_local std::vector scratch; auto ensure_scratch = [&](size_t n) { if (scratch.size() < n) scratch.resize(n); }; while (true) { // Termination test if (an == 0) { if (nnn == 1 && np[0] == 1) return jacobi_finish_or_fail(bits); return 0; } if (nnn == 0) { if (an == 1 && ap[0] == 1) return jacobi_finish_or_fail(bits); return 0; } if (an == 1 && ap[0] == 1) return jacobi_finish_or_fail(bits); if (nnn == 1 && np[0] == 1) return jacobi_finish_or_fail(bits); // 1-limb range: u64 fast path if (an == 1 && nnn == 1) { return jacobiSymbol_finish_u64(ap[0], np[0], bits); } // padding: align ap, np to a common width size_t width = std::max(an, nnn); if (an < width) std::memset(ap + an, 0, (width - an) * sizeof(uint64_t)); if (nnn < width) std::memset(np + nnn, 0, (width - nnn) * sizeof(uint64_t)); // Recursive HGCD batch (large size) — Phase 3 // Jacobi-specific threshold: unlike GCD, the bits state machine adds overhead, so // recursion starts at a higher threshold than plain hgcd2_jacobi // 384 = 24K-bit cutoff. Up to 512, plain hgcd2_jacobi is fastest; // beyond that, recursive HGCD wins (per benchmarks) constexpr size_t JACOBI_HGCD_THRESHOLD = 384; if (width >= JACOBI_HGCD_THRESHOLD) { auto& arena = getThreadArena(); size_t mark = arena.mark(); mpn::HgcdMatrix M; mpn::hgcd_matrix_init(&M, 2 * width + 4); uint64_t* hgcd_tp = arena.alloc_limbs(std::max(width, M.alloc) + 16); size_t new_n = mpn::hgcd_jacobi(ap, np, width, &M, hgcd_tp, &bits); arena.rewind(mark); if (new_n > 0) { an = mpn::normalized_size(ap, new_n); nnn = mpn::normalized_size(np, new_n); if (bits == mpn::BITS_FAIL) return 0; continue; } // No progress -> fall through to hgcd2_jacobi } // Try hgcd2_jacobi (batch over the top 2 limbs) if (width >= 2) { uint64_t mask = ap[width-1] | np[width-1]; if (mask == 0) { // Both top limbs are 0: reduce width and retry width--; an = std::min(an, width); nnn = std::min(nnn, width); continue; } bool try_hgcd2 = (width >= 2); uint64_t ah, al, bh, bl; if (try_hgcd2) { if (mask >> 63) { ah = ap[width-1]; al = (width >= 2) ? ap[width-2] : 0; bh = np[width-1]; bl = (width >= 2) ? np[width-2] : 0; } else { int shift = std::countl_zero(mask); if (width >= 3) { ah = (ap[width-1] << shift) | (ap[width-2] >> (64-shift)); al = (ap[width-2] << shift) | (ap[width-3] >> (64-shift)); bh = (np[width-1] << shift) | (np[width-2] >> (64-shift)); bl = (np[width-2] << shift) | (np[width-3] >> (64-shift)); } else if (width == 2) { ah = (ap[1] << shift) | (ap[0] >> (64-shift)); al = ap[0] << shift; bh = (np[1] << shift) | (np[0] >> (64-shift)); bl = np[0] << shift; } else { try_hgcd2 = false; } } } if (try_hgcd2) { mpn::HgcdMatrix1 M; if (mpn::hgcd2_jacobi(ah, al, bh, bl, &M, &bits)) { ensure_scratch(width + 1); size_t new_w = mpn::hgcd_mul_matrix1_vector(&M, scratch.data(), ap, np, width); std::memcpy(ap, scratch.data(), new_w * sizeof(uint64_t)); an = mpn::normalized_size(ap, new_w); nnn = mpn::normalized_size(np, new_w); if (bits == mpn::BITS_FAIL) return 0; continue; } } } // subdiv fallback: bigger -= q * smaller, jacobi_update with d, q&3 // d=1: a (=ap) shrinks (b is denominator); d=0: b (=np) shrinks (a is denominator) { int cmp_result; if (an > nnn) cmp_result = 1; else if (an < nnn) cmp_result = -1; else cmp_result = mpn::cmp(ap, an, np, nnn); if (cmp_result == 0) { // ap == np: gcd. If 1, jacobi_finish; otherwise 0 if (an == 1 && ap[0] == 1) return jacobi_finish_or_fail(bits); return 0; } uint64_t* rp; uint64_t* lp; size_t rn, ln; int d; if (cmp_result > 0) { rp = ap; rn = an; lp = np; ln = nnn; d = 1; } else { rp = np; rn = nnn; lp = ap; ln = an; d = 0; } size_t qn_cap = rn - ln + 1; size_t div_sz = mpn::divide_scratch_size(rn, ln); size_t need = ln + qn_cap + div_sz; ensure_scratch(need); uint64_t* tp = scratch.data(); // remainder buffer (size ln) uint64_t* qp = tp + ln; // quotient (size qn_cap) uint64_t* dp = qp + qn_cap; // div scratch mpn::divide(qp, tp, rp, rn, lp, ln, dp); size_t rem_n = mpn::normalized_size(tp, ln); size_t qn = mpn::normalized_size(qp, qn_cap); if (rem_n == 0) { // gcd = lp. If 1, finish; otherwise 0 if (qn > 0) bits = mpn::jacobi_update(bits, d, qp[0] & 3); if (ln == 1 && lp[0] == 1) return jacobi_finish_or_fail(bits); return 0; } if (qn > 0) { bits = mpn::jacobi_update(bits, d, qp[0] & 3); if (bits == mpn::BITS_FAIL) return 0; } // rp ← remainder std::memcpy(rp, tp, rem_n * sizeof(uint64_t)); if (rem_n < rn) std::memset(rp + rem_n, 0, (rn - rem_n) * sizeof(uint64_t)); if (cmp_result > 0) an = rem_n; else nnn = rem_n; } } } // bits termination: 0 if BITS_FAIL, otherwise jacobi_finish static int jacobi_finish_or_fail(unsigned bits) { return bits == mpn::BITS_FAIL ? 0 : mpn::jacobi_finish(bits); } static int jacobiSymbol(const Int& a, const Int& n) { // n must be a positive odd integer if (n <= 0 || n.isEven()) return 0; if (n.isOne()) return 1; // Reduce a modulo n (0 <= a_mod < n) Int a_mod = a % n; if (a_mod.isNegative()) a_mod += n; if (a_mod.isZero()) return 0; if (a_mod.isOne()) return 1; // mpn path: for >= 3 limbs, go to the mpn-level loop (avoid Int construction/normalize overhead) if (a_mod.size() >= 3 || n.size() >= 3) { std::vector aa(a_mod.data(), a_mod.data() + a_mod.size()); std::vector nn(n.data(), n.data() + n.size()); // Expand aa's capacity to match nn (Lehmer-matrix application requires zero padding) if (aa.size() < nn.size()) aa.resize(nn.size(), 0); if (nn.size() < aa.size()) nn.resize(aa.size(), 0); // bits init: a = a_mod (numerator), b = n (denominator), s = 0 unsigned bits = mpn::jacobi_init(static_cast(aa[0] & 3), static_cast(nn[0] & 3), 0); return jacobiSymbol_mpn_loop(aa.data(), a_mod.size(), nn.data(), n.size(), bits); } // Small size (<= 2 limbs): the naive Int-level implementation is fast enough Int aa = a_mod; Int nn = n; int result = 1; while (true) { if (aa.isZero()) return 0; if (aa.isOne()) return result; if (aa.bitLength() <= 64 && nn.bitLength() <= 64) { return jacobiSymbol_u64(aa.word(0), nn.word(0), result); } while (aa.isEven()) { aa >>= 1; int n8 = static_cast(nn.word(0) & 7); if (n8 == 3 || n8 == 5) result = -result; } if (aa.isOne()) return result; int a4 = static_cast(aa.word(0) & 3); int n4 = static_cast(nn.word(0) & 3); if (a4 == 3 && n4 == 3) result = -result; Int temp = aa; aa = nn % temp; nn = temp; } } /** * @brief Legendre symbol (a/p) * * When p is an odd prime, determines whether a is a quadratic residue modulo p. * A wrapper around the Jacobi symbol. * * @param a Arbitrary integer * @param p Odd prime * @return -1: non-residue, 0: p | a, 1: residue */ static int legendreSymbol(const Int& a, const Int& p) { return jacobiSymbol(a, p); } /** * @brief Extended Kronecker symbol (a/n) * * The extended Jacobi symbol, defined also for n = 0, 1, 2, and negative integers. * * @param a Arbitrary integer * @param n Arbitrary integer * @return Value of the Kronecker symbol */ static int kroneckerSymbol(const Int& a, const Int& n) { if (n.isZero()) { // (a/0) = 1 if |a|=1, 0 otherwise Int abs_a = a.isNegative() ? -a : a; return abs_a.isOne() ? 1 : 0; } Int nn = n; int result = 1; // Negative n: (a/-1) = -1 if a < 0, 1 otherwise if (nn.isNegative()) { nn = -nn; if (a.isNegative()) result = -result; } // Factor out 2 from n: (a/2) int v = 0; while (nn.isEven()) { nn >>= 1; ++v; } if (v > 0) { // (a/2^v) // (a/2) = 0 if a is even, (-1)^{(a²-1)/8} if a is odd Int abs_a = a.isNegative() ? -a : a; if (abs_a.isEven()) return 0; if (v & 1) { int a8 = static_cast(abs_a.word(0) & 7); // toUInt64 throws for 2+ limbs if (a8 == 3 || a8 == 5) result = -result; } } // Done if nn is 1 if (nn.isOne()) return result; // What remains is the Jacobi symbol for the odd part return result * jacobiSymbol(a, nn); } }; } // namespace sangi