// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // IntAPRCL.cpp — APRCL (Adleman-Pomerance-Rumely-Cohen-Lenstra) deterministic primality test // // Algorithm overview: // 1. Run the Jacobi sum test over the cyclotomic ring Z[ζ_q]/(n) // 2. All tests pass → prime factors of n are restricted to the form n^j mod e (e > √n) // 3. Final factor search: try factors of n within the orbit {n^j mod e} // // References: // H. Cohen, "A Course in Computational Algebraic Number Theory", §9.1 // R. Crandall & C. Pomerance, "Prime Numbers: A Computational Perspective", §4.4 #include #include #include #include #include #include namespace sangi { namespace { // ── Small-number primality test (for helper primes) ── bool isSmallPrime(int n) { if (n < 2) return false; if (n < 4) return true; if (n % 2 == 0 || n % 3 == 0) return false; for (int i = 5; i * i <= n; i += 6) if (n % i == 0 || n % (i + 2) == 0) return false; return true; } // ── Find the smallest prime r satisfying q | r-1 ── int findHelperPrime(int q) { for (int r = q + 1; ; r += q) if (isSmallPrime(r)) return r; } // ── Primitive root mod r (r is a small prime) ── int primitiveRoot(int r) { if (r == 2) return 1; // Prime factorization of φ(r) = r-1 int phi = r - 1; int factors[16]; int nf = 0; { int tmp = phi; for (int p = 2; p * p <= tmp; ++p) { if (tmp % p == 0) { factors[nf++] = p; while (tmp % p == 0) tmp /= p; } } if (tmp > 1) factors[nf++] = tmp; } for (int g = 2; g < r; ++g) { bool ok = true; for (int fi = 0; fi < nf; ++fi) { // Verify g^{phi/factors[fi]} mod r ≠ 1 long long pw = 1; int exp = phi / factors[fi]; long long base = g; for (int e = exp; e > 0; e >>= 1) { if (e & 1) pw = pw * base % r; base = base * base % r; } if (pw == 1) { ok = false; break; } } if (ok) return g; } return -1; } // ── Perfect-power test: is n = a^b (b ≥ 2)? ── bool isPerfectPower(const Int& n) { if (n <= Int(3)) return false; int maxExp = static_cast(n.bitLength()); for (int b = 2; b <= maxExp; ++b) { if (!isSmallPrime(b)) continue; if (b == 2) { Int a = IntOps::sqrt(n); if (a * a == n) return true; continue; } // Find a = n^{1/b} by binary search size_t aBits = (n.bitLength() + b - 1) / b; if (aBits == 0) break; Int lo(2); Int hi = Int(1) << static_cast(aBits + 1); if (hi > n) hi = n; while (lo <= hi) { Int mid = (lo + hi) >> 1; // Compute mid^b Int pw(1); bool overflow = false; for (int i = 0; i < b; ++i) { pw *= mid; if (pw > n) { overflow = true; break; } } if (!overflow && pw == n) return true; if (overflow || pw > n) hi = mid - 1; else lo = mid + 1; } } return false; } // ══════════════════════════════════════════════════════════════════ // UnityZp: element of Z[ζ_q]/(n) (q is prime) // // ζ_q is a primitive q-th root of unity. From Φ_q(ζ) = 0, ζ^{q-1} = -(1+ζ+...+ζ^{q-2}). // An element is represented by q-1 Int coefficients: c[0] + c[1]ζ + ... + c[q-2]ζ^{q-2} // ══════════════════════════════════════════════════════════════════ class UnityZp { public: int q; // prime order int dim; // q - 1 (number of coefficients) Int mod; // modulus n std::vector c; // dim coefficients UnityZp() : q(0), dim(0) {} UnityZp(int q_, const Int& n_) : q(q_), dim(q_ - 1), mod(n_), c(q_ - 1) {} // Multiplicative identity (1) static UnityZp one(int q, const Int& n) { UnityZp u(q, n); u.c[0] = Int(1); return u; } // Multiplication: product within Z[ζ_q]/(n) UnityZp operator*(const UnityZp& rhs) const { UnityZp result(q, mod); // Wrap the polynomial product with x^q = 1 → q coefficients std::vector buf(q); for (int i = 0; i < dim; ++i) { if (c[i].isZero()) continue; for (int j = 0; j < dim; ++j) { if (rhs.c[j].isZero()) continue; int pos = (i + j) % q; buf[pos] += c[i] * rhs.c[j]; } } // Normalize mod n for (auto& x : buf) x = IntModular::mod(x, mod); // Φ_q reduction: subtract buf[q-1] from all coefficients // (ζ^{q-1} = -(1 + ζ + ... + ζ^{q-2})) for (int j = 0; j < dim; ++j) result.c[j] = IntModular::mod(buf[j] - buf[q - 1], mod); return result; } // Exponentiation: left-to-right binary method UnityZp pow(const Int& exp) const { if (exp.isZero()) return one(q, mod); int bits = static_cast(exp.bitLength()); UnityZp result = one(q, mod); for (int i = bits - 1; i >= 0; --i) { result = result * result; if (exp.getBit(i)) result = result * (*this); } return result; } // Galois automorphism σ_s: ζ → ζ^s UnityZp sigma(int s) const { // Map coefficient c[i] to position (i*s) mod q std::vector buf(q); for (int i = 0; i < dim; ++i) { if (c[i].isZero()) continue; int pos = static_cast((static_cast(i) * s) % q); if (pos < 0) pos += q; buf[pos] += c[i]; } // Φ_q reduction + mod n UnityZp result(q, mod); for (int j = 0; j < dim; ++j) result.c[j] = IntModular::mod(buf[j] - buf[q - 1], mod); return result; } bool operator==(const UnityZp& rhs) const { if (q != rhs.q) return false; for (int i = 0; i < dim; ++i) if (c[i] != rhs.c[i]) return false; return true; } bool operator!=(const UnityZp& rhs) const { return !(*this == rhs); } }; // ── Computation of the Jacobi sum J(χ,χ) ── // χ is a character of order q mod the helper prime r // J(χ,χ) = Σ_{a=2}^{r-1} ζ_q^{ind(a) + ind(1-a)} UnityZp jacobiSum(int q, int r, const Int& n) { int g = primitiveRoot(r); // Discrete logarithm table: ind[a] = log_g(a) mod r std::vector ind(r, -1); long long ga = 1; for (int i = 0; i < r - 1; ++i) { ind[static_cast(ga)] = i; ga = ga * g % r; } // Coefficients of the raw polynomial (accumulated at positions 0..q-1) std::vector raw(q); for (int a = 2; a < r; ++a) { int b = ((1 - a) % r + r) % r; if (b == 0) continue; // χ(0) = 0 (excluded) int e = (ind[a] + ind[b]) % q; raw[e] += 1; } // Φ_q reduction UnityZp J(q, n); for (int j = 0; j < q - 1; ++j) J.c[j] = IntModular::mod(raw[j] - raw[q - 1], n); return J; } // ── Parameter selection ── struct APRCLParam { int q; // prime int helperPrime; // prime satisfying q | helperPrime - 1 }; // Add primes until e = lcm(q_i) > √n std::vector selectParams(const Int& n) { std::vector params; Int e(1); Int target = IntOps::sqrt(n) + 1; // Add primes in increasing order (small primes first → efficient) // q=2 is special (Z[ζ_2] = Z, trivial), so start from q=3 for (int q = 3; q < 10000; q += 2) { if (e > target) break; if (!isSmallPrime(q)) continue; int r = findHelperPrime(q); params.push_back({q, r}); // e = lcm(e, q) — q is prime, so e*q if q ∤ e, otherwise e Int qi(q); Int g = IntGCD::gcd(e, qi); e = e * qi / g; } return params; } } // anonymous namespace // ══════════════════════════════════════════════════════════════════ // isProvablePrime — APRCL deterministic primality test // ══════════════════════════════════════════════════════════════════ bool IntPrime::isProvablePrime(const Int& n) { // ── Special cases ── if (n.isSpecialState()) return false; if (n <= 1) return false; if (n == 2 || n == 3) return true; if (n.isEven()) return false; // ── Small numbers: deterministic via Miller-Rabin ── // For n < 3.3×10^24 (≈ 82 bits), deterministic with specific bases if (n.bitLength() <= 82) { return isMillerRabinPrime(n, 40); } // ── Perfect-power check ── if (isPerfectPower(n)) return false; // ── Parameter selection ── auto params = selectParams(n); // Compute e = lcm(q_i) Int e(1); for (auto& p : params) { Int qi(p.q); Int g = IntGCD::gcd(e, qi); e = e * qi / g; } // ── Preprocessing: gcd(n, e) check ── Int ge = IntGCD::gcd(n, e); if (!ge.isOne()) { // n is divisible by a small prime return n == ge; } // ── Jacobi sum test ── for (auto& param : params) { // gcd(n, helperPrime) check Int gr(param.helperPrime); Int nmod_r = IntModular::mod(n, gr); if (nmod_r.isZero()) { return n == gr; } // Compute the Jacobi sum J(χ,χ) UnityZp J = jacobiSum(param.q, param.helperPrime, n); // Compute J^n mod n UnityZp Jn = J.pow(n); // Compute σ_{n mod q}(J) Int nmodq = IntModular::mod(n, Int(param.q)); int s = nmodq.toInt(); UnityZp sigmaJ = J.sigma(s); // Test: J^n ≡ σ_s(J) (mod n) if (Jn != sigmaJ) { return false; // confirmed composite } } // ── Final factor search ── // Jacobi sum test passed → every prime factor p of n satisfies p ≡ n^j (mod e) // Since e > √n, p < e → p = n^j mod e // Search for factors of n within the orbit {n^j mod e : j ≥ 0} Int nmod = IntModular::mod(n, e); Int cur(1); // n^0 mod e Int one_val(1); // Orbit length = ord_e(n), at most φ(e) // Set a safety bound (ECPP should be used for very large numbers) constexpr size_t MAX_ORBIT = 10000000; for (size_t j = 0; j < MAX_ORBIT; ++j) { if (j > 0 && cur == one_val) break; // completed a full cycle if (cur > one_val && cur < n) { // Check whether cur is a factor of n Int g = IntGCD::gcd(cur, n); if (g > one_val && g < n) return false; // nontrivial factor found → composite } cur = IntModular::mod(cur * nmod, e); } return true; // no factor → confirmed prime } } // namespace sangi