// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // IntNumberTheory.hpp // 数論的関数(約数列挙、メビウス関数、オイラーのファイ関数、Jacobi記号) // 移植元: lib++20 整数.素数.cpp #pragma once #include "IntBase.hpp" #include "IntFactorTable.hpp" #include "IntGCD.hpp" #include #include namespace calx { /** * @brief 数論的関数 * * 素因数分解に基づく数論的関数を提供します。 * IntFactorTable を利用して素因数分解を行い、 * 約数列挙・メビウス関数・オイラーのファイ関数を計算します。 */ class IntNumberTheory { public: /** * @brief n の全ての正の約数を昇順で返す * * @param n 正整数 (n <= 0 の場合は空ベクトル) * @param table 素因数テーブル * @return 約数のベクトル(昇順ソート済み) * * アルゴリズム: * 素因数分解 n = p1^e1 * p2^e2 * ... * pk^ek から * 各素因数の冪 0..ei の全組み合わせを再帰的に生成する。 * * 例: * 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 <= Int(0)) return {}; auto factors = table.factorize(n); if (factors.empty()) return { Int(1) }; // n == 1 // 約数の個数を計算 size_t count = 1; for (auto& [p, e] : factors) count *= static_cast(e + 1); // 約数を生成: 各素因数の冪の全組み合わせ 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 メビウス関数 μ(n) * * @param n 正整数 (n <= 0 → 0) * @param table 素因数テーブル * @return μ(n): * - μ(1) = 1 * - μ(n) = 0 if n が平方因子を持つ * - μ(n) = (-1)^k if n = p1*p2*...*pk (異なる素数の積) * * 参考: 芹沢正三「素数入門」p206 */ static int moebiusMu(const Int& n, const IntFactorTable& table) { if (n <= Int(0)) return 0; if (n == Int(1)) return 1; auto factors = table.factorize(n); // 平方因子を持つか確認 for (auto& [p, e] : factors) { if (e >= 2) return 0; } // 異なる素因数の数が偶数個なら +1, 奇数個なら -1 return (factors.size() & 1) ? -1 : 1; } /** * @brief オイラーのファイ関数 φ(n) * * n と互いに素な n 以下の正整数の個数を返す。 * * @param n 正整数 (n <= 0 → 0) * @param table 素因数テーブル * @return φ(n) = n * Π_{p|n} (1 - 1/p) * * アルゴリズム: * φ(n) = n * Π (1 - 1/p_i) = n - Σ n/p_i + ... * 整数演算で実装: result = n, 各素因数 p に対して result -= result / p * * 参考: 芹沢正三「素数入門」p200 */ static Int eulerPhi(const Int& n, const IntFactorTable& table) { if (n <= Int(0)) return Int(0); if (n == Int(1)) return Int(1); auto factors = table.factorize(n); Int result = n; for (auto& [p, e] : factors) result -= result / p; return result; } /** * @brief 約数の個数 σ_0(n) * * @param n 正整数 * @param table 素因数テーブル * @return n の約数の個数 */ static Int divisorCount(const Int& n, const IntFactorTable& table) { if (n <= Int(0)) return Int(0); if (n == Int(1)) return Int(1); auto factors = table.factorize(n); Int count(1); for (auto& [p, e] : factors) count *= Int(e + 1); return count; } /** * @brief 約数の総和 σ_1(n) * * @param n 正整数 * @param table 素因数テーブル * @return n の全約数の和 * * アルゴリズム: * σ(n) = Π_{p^e || n} (p^{e+1} - 1) / (p - 1) */ static Int divisorSum(const Int& n, const IntFactorTable& table) { if (n <= Int(0)) return Int(0); if (n == Int(1)) 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 記号 / Legendre 記号 / Kronecker 記号 // ===================================================================== /** * @brief Jacobi 記号 (a/n) * * n が奇数正整数のとき、Jacobi 記号 (a/n) を計算します。 * n が素数のとき Legendre 記号に一致します。 * * アルゴリズム: 二次相互法則に基づく反復法 * 1. a = a mod n に簡約 * 2. a が偶数なら 2 の因子を取り出し (2/n) を計算 * 3. 二次相互法則で (a/n) = ±(n/a) と反転 * 4. a と n を入れ替えて繰り返し * * @param a 任意の整数 * @param n 奇数正整数 (n > 0, n は奇数) * @return -1, 0, 1 のいずれか * * 特殊ケース: * jacobi(0, n) = 0 (n > 1), jacobi(0, 1) = 1 * jacobi(1, n) = 1 * n が偶数または n <= 0 → 0 (未定義) */ static int jacobiSymbol(const Int& a, const Int& n) { // n は奇数正整数でなければならない if (n <= Int(0) || n.isEven()) return 0; if (n == Int(1)) return 1; // a を 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 == Int(1)) return 1; Int aa = a_mod; Int nn = n; int result = 1; while (true) { if (aa.isZero()) return 0; if (aa == Int(1)) return result; // 2 の因子を取り出す // (2/n) = (-1)^{(n²-1)/8} // n mod 8 が 3 or 5 のとき (2/n) = -1 while (aa.isEven()) { aa >>= 1; // n mod 8 int n8 = static_cast(nn.toUInt64() & 7); if (n8 == 3 || n8 == 5) result = -result; } if (aa == Int(1)) return result; // 二次相互法則: (a/n) = (-1)^{(a-1)(n-1)/4} * (n/a) // a ≡ 3 (mod 4) かつ n ≡ 3 (mod 4) のとき符号反転 int a4 = static_cast(aa.toUInt64() & 3); int n4 = static_cast(nn.toUInt64() & 3); if (a4 == 3 && n4 == 3) result = -result; // swap(aa, nn), aa = nn mod aa Int temp = aa; aa = nn % temp; nn = temp; } } /** * @brief Legendre 記号 (a/p) * * p が奇素数のとき、a が p を法とする二次剰余かどうかを判定します。 * Jacobi 記号のラッパーです。 * * @param a 任意の整数 * @param p 奇素数 * @return -1: 非剰余, 0: p|a, 1: 剰余 */ static int legendreSymbol(const Int& a, const Int& p) { return jacobiSymbol(a, p); } /** * @brief Kronecker 記号 (a/n) の拡張 * * n = 0, 1, 2, 負数に対しても定義される拡張 Jacobi 記号。 * * @param a 任意の整数 * @param n 任意の整数 * @return Kronecker 記号の値 */ 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 == Int(1)) ? 1 : 0; } Int nn = n; int result = 1; // 負の n: (a/-1) = -1 if a < 0, 1 otherwise if (nn.isNegative()) { nn = -nn; if (a.isNegative()) result = -result; } // n の因子 2 を取り出す: (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.toUInt64() & 7); if (a8 == 3 || a8 == 5) result = -result; } } // nn が 1 なら完了 if (nn == Int(1)) return result; // 残りは奇数部分に対する Jacobi 記号 return result * jacobiSymbol(a, nn); } }; } // namespace calx