模数为 2 的幂次的情形较为特殊.为处理这种情形,需要用到关于模 2e 既约剩余系结构的一个 结论:所有奇数 a 都唯一地同余于某个 (−1)s5rmod2e 形式的整数,其中,s∈{0,1} 且 0≤r<2e−2.借助这一结果,可以得到如下结论:
定理
设整数 k≥2,奇数 a 和正整数 m=2e 且 e≥2.那么,当 k 是奇数时,有:
a 恒为模 m 的 k 次剩余.
a 模 m 的 k 次方根有且仅有一个.
模 m 的 k 次剩余类个数为 2e−1,且它们就是全体既约剩余类.
当 k 是偶数时,记 d=gcd(k,2e−2) 且 d′=d2e−2,有:
a 为模 m 的 k 次剩余,当且仅当 a≡1(mod4) 且 ad′≡1(modm).
当 a 为模 m 的 k 次剩余时,同余意义下,a 模 m 恰有 2d 个互不相同的 k 次方根,且它们具有形式
x≡±5y0+id′(mod2e−1),0≤y0<d′,i=0,1,⋯,d−1.
模 m 的 k 次剩余类的个数为 d′,且它们的全体就是
{5dimodm:0≤i<d′}.
证明
因为 a⊥m,所以 x⊥m.因为 x 和 a 都是奇数,由前述结论可知,可以设 a≡(−1)s5r(mod2e) 且 x=(−1)z5y(mod2e).因为表示是唯一的,所以同余方程 xk≡a(mod2e) 等价于 线性同余方程 组
kzky≡s(mod2),≡r(mod2e−2).
结合该页面对于线性同余方程解的分析,就可以得到同余方程 xk≡a(mod2e) 解的结构.根据 k 的奇偶性不同,可以分为两种情形:
当 k 是奇数时,因为 gcd(k,2)=gcd(k,2e−2)=1,所以两个线性同余方程对于所有 s,r 都有解,故而原同余方程对于所有奇数 a 总是有解.
当 k 是偶数时,第一个方程有解当且仅当 2∣s,第二个方程有解当且仅当 d=gcd(k,2e−2)∣r.将两者结合就得到 k 次剩余类的全体形式.直接计算可知,第一个条件等价于 a≡1(mod4);重复奇素数幂情形的分析可知,第二个条件等价于 ad′=1.将两点结合起来就得到定理中的判定方法.两个线性同余方程的通解也是已知的:
zy≡0,1(mod2),≡y0+id′(mod2e−2),0≤y0<2e−2.
将两者结合就得到原方程的通解.
这就完全解决了不同模数下 k 次剩余的判定问题.二次剩余中的 Legendre 记号和二次互反律等内容也可以推广到高次剩余的情形,但这并不容易,需要用到 分圆域 等概念.在代数数论中,二次互反律最终可以推广到 Artin 互反律.
单位根
作为 k 次方根的特殊情形,本节讨论 k 次(本原)单位根的概念.它可以看作是复数域 C 中 k 次 单位根 的概念在模 m 既约剩余系 Zm∗ 中的对应.当模数 m 合适时,用模 m 的 k 次本原单位根代替复数根 ωk 可以加速计算.
类似于复数域的情形,有如下定义:
模 m 的 k 次单位根
对于模数 m,元素 1 的 k 次方根称为 模 m 的 k 次单位根(k-th root of unity modulo m).特别地,如果 x 是模 m 的一个 k 次单位根,且它不是模 m 的任何 k′<k 次单位根,那么,也称 x 为 模 m 的 k 次本原单位根(k-th primitive root of unity modulo m).
比较 原根的定义 可知,原根 g 就是模 m 的 φ(m) 次本原单位根,其中,φ(m) 是 欧拉函数.
当模 m 的 k 次本原单位根存在时,它的代数性质和 k 次本原单位复根 ωk 一致,可以代替 ωk 进行各种计算.例如,将它应用于 快速傅里叶变换 中,就得到有限域 1 上的 快速数论变换.
所有与 m 互素的整数 a 都是模 m 的 δm(a) 次本原单位根,其中,δm(a) 是 a 模 m 的 阶.
元素 a 是模 m 的 k 次单位根,且 k′ 是 k 的任意倍数,那么 a 也是模 m 的 k′ 次单位根.
元素 a 是模 m 的 k 次(本原)单位根,那么元素 aℓ 是模 m 的 gcd(k,ℓ)k 次(本原,相应地)单位根.
当 k′ 遍历 k 的因数,所有模 m 的 k′ 次本原单位根恰构成模 m 的 k 次单位根的一个划分.而且,对于 ℓ⊥k,映射 x↦xℓ 给出 k 次单位根之间的双射,且保持上述划分不变:它将 k′∣k 次本原单位根仍然映射到 k′ 次本原单位根.
模 m 的 k 次本原单位根存在,当且仅当 k∣λ(m).特别地,模 m 的 λ(m) 次本原单位根存在,称为 模 m 的 λ‑原根.
元素 a 是模 m 的 k 次单位根,当且仅当 ak≡1(modm) 且对于任意素因子 p∣k 都有 ak/p≡1(modm).
证明
根据阶的定义,所有与 m 互素的整数 a 都是模 m 的 δm(a) 次本原单位根,其中,δm(a) 是 a 模 m 的阶.反过来,如果 a 是模 m 的 k 次单位根,那么 gcd(ak,m)=1,所以 gcd(a,m)=1.因此,a 是模 m 的(本原)单位根,当且仅当 a 与 m 互素.这就是性质 1.
从这些性质可以看出,相对于原根存在的情形,模 m 的 λ‑原根起到了类似的基础作用.与原根不同的是,λ‑原根的幂次并不能用于生成模 m 的全体单位根.尽管如此,由于 λ‑原根的密度并不低 2,如果确实需要找到 k 次本原单位根,可以首先通过随机方法找到一个 λ‑原根,再通过求幂次得到一个 k 次本原单位根.
如果已知 a 模 m 的一个 k 次方根,可以通过模 m 的全体 k 次单位根生成 a 模 m 的全体 k 次方根.
定理
设 x 是 a 模 m 的一个 k 次方根,当 r 遍历模 m 的全体 k 次单位根时,xr 遍历 a 模 m 的全体 k 次方根.
证明
对于 a 模 m 的两个 k 次方根 x,y,设 r=x−1ymodm,那么 r 满足 rk≡1(modm),是模 m 的 k 次方根.反过来,只要 r 是模 m 的 k 次单位根,那么,(xr)k=xkrk≡a(modm),也就是说,xr 是模 m 的 k 次方根.
利用 k 次单位根生成全体 k 次方根,就类似于利用齐次线性方程组的解生成非齐次线性方程组的通解一样.
前面讨论的是一般情形.仅对于原根存在的情形,单位根的结构更为简单:
定理
对于模数 m,设模 m 的原根存在,且 a 是模 m 的 k 次本原单位根.那么,b 是模 m 的 k 次单位根,当且仅当它可以表示为 a 的幂次.
证明
设 g 是模 m 的原根,那么,所有与 m 互素的元素都可以表示为 g 的幂次.那么,a 是模 m 的 k 次本原单位根,当且仅当
// Submission (TLE): https://judge.yosupo.jp/submission/320582#include <algorithm>#include <cmath>#include <iostream>#include <tuple>#include <unordered_map>#include <vector>struct PrimePower { int p, e, pe; PrimePower(int p, int e, int pe) : p(p), e(e), pe(pe) {}};// Factorization.auto factorize(int n) { std::vector<PrimePower> ans; for (int x = 2; x * x <= n; ++x) { int e = 0, pe = 1; for (; n % x == 0; n /= x, ++e, pe *= x); if (e) ans.emplace_back(x, e, pe); } if (n > 1) ans.emplace_back(n, 1, n); return ans;}// Binary exponentiation.int pow(int a, int b, int m = 0) { int res = 1; for (; b; b >>= 1) { if (b & 1) res = m ? (long long)res * a % m : res * a; a = m ? (long long)a * a % m : a * a; } return res;}// Find a primitive root modulo prime.int primitive_root(int p) { std::vector<int> exp; for (auto factor : factorize(p - 1)) { exp.push_back((p - 1) / factor.p); } int ans = 0; bool succ = false; while (!succ) { ++ans; succ = true; for (int b : exp) { if (pow(ans, b, p) == 1) { succ = false; break; } } } return ans;}// Discrete logarithm. (BSGS Algorithm)int log(int g, int a, int m) { int b = std::sqrt(m + 0.25l) + 1; std::unordered_map<int, int> mp; int po0 = a % m, po1 = 1; for (int i = 0; i < b; ++i) { mp[po0] = i; po0 = (long long)po0 * g % m; } po0 = pow(g, b, m); for (int j = 1; j <= b; ++j) { po1 = (long long)po1 * po0 % m; if (mp.count(po1)) return j * b - mp[po1]; } return -1;}// Extended Euclidean Algorithm.int ex_gcd(int a, int b, int& x, int& y) { if (!b) { x = 1; y = 0; return a; } else { int d = ex_gcd(b, a % b, y, x); y -= a / b * x; return d; }}// Solves the linear congruence equation: Ax = B mod N.// Return the least nonnegative solution and the common difference.std::pair<int, int> solve_linear(int a, int b, int n) { int x, y; int d = ex_gcd(a, n, x, y); if (b % d) return {-1, -1}; n /= d; x = ((long long)x * (b / d) % n + n) % n; return {x, n};}// Subroutine: Find a K-th root with a primitive root G known.int calc(int g, int k, int a, int p) { int ind = log(g, a, p); if (ind == -1) return -1; int y0, d; std::tie(y0, d) = solve_linear(k, ind, p - 1); if (y0 == -1) return -1; return pow(g, y0, p);}// Find a K-th root of A modulo prime P.int kth_roots_mod_p(int k, int a, int p) { a %= p; if (k == 0) return a == 1 ? 0 : -1; if (a == 0) return 0; return calc(primitive_root(p), k, a, p);}void solve() { int k, y, p; std::cin >> k >> y >> p; std::cout << kth_roots_mod_p(k, y, p) << '\n';}int main() { int t; std::cin >> t; for (; t; --t) { solve(); }}
Tonelli–Shanks 算法的核心想法是,将离散对数的求解放到阶为 2e 的群里,进而降低时间复杂度.类似地,对于任意素数幂 pe 阶群内的离散对数,同样可以较为高效地求解,但是算法的复杂度为 Ω(p).Adleman–Manders–Miller 算法将 k 次方根的求解分拆为多个素数幂阶群内离散对数的计算,但是受限于 k 的最大素因子 pmax(k) 的大小,算法复杂度仍然为 Ω(pmax(k)).本节算法进一步改良了这一过程,避免了对较大的素因子计算离散对数,进而将整体复杂度控制到 O(m1/4+ε).
过程
考虑素数幂模 m 下 a 的 k 次方根的计算,即求解同余方程:
xk≡a(modm).
特别地,对于 m=2e 的情形,还需要保证 a≡1(mod4),进而 a 可以写成 g=5 的幂次.类似前文讨论,模 2e 下 k 次方根的计算总是可以转化为这样的情形.处理模 2e 的情形时,本节提到的 φ(m) 都应换作 δm(5)=2e−2.
首先,问题可以转化为开方次数整除 φ(m) 的情形.设 d=gcd(k,φ(m)).那么,由 k 次剩余的性质可知,当 a 模 m 是 k 次剩余时,a 总是模 m 的 dφ(m) 次单位根.根据单位根的性质,对于任意 ℓ⊥dφ(m),映射 x↦xℓ 都是 dφ(m) 次单位根之间的双射.因此,可以取
事实上,在这一情景中,无需使用 Pollard Rho 算法分解素因数,仍然可以获得 O(m1/4+ε) 的时间复杂度.事实上,只需要对 d 暴力试除进行分解,并只枚举到不超过 m1/4 的素因子.设去除这些小素因子后得到的整数为 z.那么,对于 z 的素因子 p>m1/4,必然有 νp(φ(m))<4,其中,νp(n) 表示 n 的素因数分解中 p 的次数.由于只需要考虑
1≤e=νp(d)<s=νp(φ(m))<4
的情形,满足该条件的素因子 p 至多只能有一个;否则,它们在 φ(m) 中的次数都不小于 2,总的乘积必然超过 m.要分离出这个(可能存在的)唯一的大素因子,只需要计算
#include <algorithm>#include <cmath>#include <iostream>#include <random>#include <tuple>#include <unordered_map>#include <vector>std::mt19937 rng(std::random_device{}());// Binary exponentiation.int pow(int a, int b, int m = 0) { int res = 1; for (; b; b >>= 1) { if (b & 1) res = m ? (long long)res * a % m : res * a; a = m ? (long long)a * a % m : a * a; } return res;}// Find a P-th non-residue mod M.int non_residue(int p, int m, int phi) { std::uniform_int_distribution<int> dis(1, m - 1); while (true) { int c = dis(rng); if (pow(c, phi / p, m) != 1) return c; } return -1;}// Euclidean Algorithm.int gcd(int a, int b) { return b ? gcd(b, a % b) : a; }// Extended Euclidean Algorithm.int ex_gcd(int a, int b, int& x, int& y) { if (!b) { x = 1; y = 0; return a; } else { int d = ex_gcd(b, a % b, y, x); y -= a / b * x; return d; }}// Returns the modular inverse of A modulo M.// Assumes that gcd(A, M) = 1, so the inverse exists.int inv(int a, int m) { int x, y; ex_gcd(a, m, x, y); return (x % m + m) % m;}// Subroutine: Find a P^E-th root of A mod M.int peth_root_mod_m(int p, int e, int a, int m, int phi) { int s = 0, r = phi, pe = pow(p, e); for (; r % p == 0; r /= p, ++s); int q = pe - inv(r, pe); int ans = pow(a, ((long long)q * r + 1) / pe % phi, m); int eta = non_residue(p, m, phi); std::unordered_map<int, int> mp; int zeta = pow(eta, r, m); int xi = pow(eta, phi / p, m); // Precompute powers for BSGS. int B = std::sqrt((s - e) * p + 0.25l) + 1; int pB = p / B + 1; int po0 = pow(xi, pB, m); for (int j = 1, po1 = 1; j <= B; ++j) { po1 = (long long)po1 * po0 % m; mp[po1] = j; } // Compute p-adic digits of h. for (int j = 0; j < s - e; ++j) { int err = (long long)pow(ans, pe, m) * inv(a, m) % m; int xi_hj = pow(err, pow(p, s - e - j - 1), m); long long hj = 0; // BSGS query. for (int i = 1; i <= pB; ++i) { xi_hj = (long long)xi_hj * xi % m; if (mp.count(xi_hj)) { hj = mp[xi_hj] * pB - i; break; } } ans = (long long)ans * pow(zeta, phi - hj * pow(p, j) % phi, m) % m; } return ans;}// Find a K-th root of A modulo prime P.int kth_root_mod_p(int k, int a, int p) { a %= p; if (k == 0) return a == 1 ? 0 : -1; if (a == 0) return 0; int d = gcd(k, p - 1); if (pow(a, (p - 1) / d, p) != 1) return -1; a = pow(a, inv(k / d, (p - 1) / d), p); for (int dp = 2; dp * dp <= d && dp * dp * dp * dp < p; ++dp) { if (d % dp == 0) { int de = 0; for (; d % dp == 0; d /= dp, ++de); a = peth_root_mod_m(dp, de, a, p, p - 1); } } if (d != 1) { int dp = gcd(d, (p - 1) / d), de = 0; if (dp != 1) { for (; d % dp == 0; d /= dp, ++de); a = peth_root_mod_m(dp, de, a, p, p - 1); } if (d != 1) a = peth_root_mod_m(d, 1, a, p, p - 1); } return a;}void solve() { int k, y, p; std::cin >> k >> y >> p; std::cout << kth_root_mod_p(k, y, p) << '\n';}int main() { int t; std::cin >> t; for (; t; --t) { solve(); }}
一般情形的处理
考虑一般的情形,仍然设模数 m 是素数幂 pe,但是 gcd(a,m)>1.如果 a≡0(modm),那么
#include <algorithm>#include <cmath>#include <iostream>#include <tuple>#include <unordered_map>#include <vector>struct PrimePower { int p, e, pe; PrimePower(int p, int e, int pe) : p(p), e(e), pe(pe) {}};// Factorization.auto factorize(int n) { std::vector<PrimePower> ans; for (int x = 2; x * x <= n; ++x) { int e = 0, pe = 1; for (; n % x == 0; n /= x, ++e, pe *= x); if (e) ans.emplace_back(x, e, pe); } if (n > 1) ans.emplace_back(n, 1, n); return ans;}// Binary exponentiation.int pow(int a, int b, int m = 0) { int res = 1; for (; b; b >>= 1) { if (b & 1) res = m ? (long long)res * a % m : res * a; a = m ? (long long)a * a % m : a * a; } return res;}// Find a primitive root modulo odd prime power.int primitive_root(PrimePower pp) { std::vector<int> exp; int phi = pp.pe / pp.p * (pp.p - 1); for (auto factor : factorize(pp.p - 1)) { exp.push_back(phi / factor.p); } if (pp.e != 1) exp.push_back(phi / pp.p); int ans = 0; bool succ = false; while (!succ) { ++ans; succ = true; for (int b : exp) { if (pow(ans, b, pp.pe) == 1) { succ = false; break; } } } return ans;}// Discrete logarithm. (BSGS Algorithm)int log(int g, int a, int m) { int b = std::sqrt(m + 0.25l) + 1; std::unordered_map<int, int> mp; int po0 = a % m, po1 = 1; for (int i = 0; i < b; ++i) { mp[po0] = i; po0 = (long long)po0 * g % m; } po0 = pow(g, b, m); for (int j = 1; j <= b; ++j) { po1 = (long long)po1 * po0 % m; if (mp.count(po1)) return j * b - mp[po1]; } return -1;}// Extended Euclidean Algorithm.int ex_gcd(int a, int b, int& x, int& y) { if (!b) { x = 1; y = 0; return a; } else { int d = ex_gcd(b, a % b, y, x); y -= a / b * x; return d; }}// Returns the modular inverse of A modulo M.// Assumes that gcd(A, M) = 1, so the inverse exists.int inv(int a, int m) { int x, y; ex_gcd(a, m, x, y); return (x % m + m) % m;}// Solves the linear congruence equation: Ax = B mod N.// Return the least nonnegative solution and the common difference.std::pair<int, int> solve_linear(int a, int b, int n) { int x, y; int d = ex_gcd(a, n, x, y); if (b % d) return {-1, -1}; n /= d; x = ((long long)x * (b / d) % n + n) % n; return {x, n};}// Subroutine: Find all the K-th roots with a primitive root G known.std::vector<int> calc(int g, int k, int a, int p, int pe) { int ind = log(g, a, pe); if (ind == -1) return {}; int mm = p == 2 ? pe / 4 : pe / p * (p - 1); int y0, d; std::tie(y0, d) = solve_linear(k, ind, mm); if (y0 == -1) return {}; int ans = pow(g, y0, pe), po = pow(g, d, pe); std::vector<int> res(mm / d); for (auto& x : res) { x = ans; ans = (long long)ans * po % pe; } return res;}// Find all the K-th roots of A modulo prime power P^E.std::vector<int> kth_roots_mod_pe(int k, int a, PrimePower pp) { int p = pp.p, e = pp.e, pe = pp.pe; a %= pe; if (a == 0) { int d = pow(p, (e - 1) / k + 1); std::vector<int> res(pe / d); for (int i = 0; i < pe / d; ++i) { res[i] = i * d; } return res; } int s = 0; for (; a % p == 0; a /= p, ++s); if (s % k) return {}; int psk = pow(p, s / k), pss = pow(p, s - s / k), pes = pow(p, e - s); std::vector<int> res; if (p != 2) { int g = primitive_root(PrimePower(p, e - s, pes)); res = calc(g, k, a, p, pes); } else if (pes == 2) { res.push_back(a); } else if (k & 1) { int z = a % 4 == 3; a = z ? pes - a : a; res = calc(5, k, a, p, pes); if (z) { for (auto& x : res) x = pes - x; } } else { if (a % 4 == 3) return {}; res = calc(5, k, a, p, pes); int m = res.size(); res.reserve(m * 2); for (int i = 0; i < m; ++i) { res.push_back(pes - res[i]); } } int m = res.size(); res.reserve(m * pss); for (int j = 1; j < pss; ++j) { for (int i = 0; i < m; ++i) { res.push_back(res.end()[-m] + pes); } } for (auto& x : res) x *= psk; return res;}// Find all the K-th roots of A modulo positive integer M.std::vector<int> kth_roots_mod_m(int k, int a, int m) { auto factors = factorize(m); int m0 = 0; std::vector<std::vector<int>> sols; for (const auto& pp : factors) { sols.push_back(kth_roots_mod_pe(k, a, pp)); if (sols.back().empty()) return {}; } std::vector<int> ans; for (int i = 0; i < (int)factors.size(); ++i) { auto pp = factors[i]; if (!i) { m0 = pp.pe; ans = sols[i]; } else { long long m1 = pp.pe * inv(pp.pe, m0); long long m2 = m0 * inv(m0, pp.pe); m0 *= pp.pe; std::vector<int> _ans; _ans.reserve(ans.size() * sols[i].size()); for (auto x : ans) { for (auto y : sols[i]) { _ans.push_back((m1 * x + m2 * y) % m0); } } ans.swap(_ans); } } return ans;}void solve() { int n, m, k; std::cin >> n >> m >> k; auto ans = kth_roots_mod_m(n, k, m); if (ans.empty()) { std::cout << 0 << '\n'; return; } std::cout << ans.size() << '\n'; std::sort(ans.begin(), ans.end()); for (auto x : ans) std::cout << x << ' '; std::cout << '\n';}int main() { int t; std::cin >> t; for (; t; --t) { solve(); }}
改良 Tonelli–Shanks 算法
#include <algorithm>#include <cmath>#include <iostream>#include <random>#include <tuple>#include <unordered_map>#include <vector>std::mt19937 rng(std::random_device{}());struct PrimePower { int p, e, pe; PrimePower(int p, int e, int pe) : p(p), e(e), pe(pe) {}};// Factorization.auto factorize(int n) { std::vector<PrimePower> ans; for (int x = 2; x * x <= n; ++x) { int e = 0, pe = 1; for (; n % x == 0; n /= x, ++e, pe *= x); if (e) ans.emplace_back(x, e, pe); } if (n > 1) ans.emplace_back(n, 1, n); return ans;}// Binary exponentiation.int pow(int a, int b, int m = 0) { int res = 1; for (; b; b >>= 1) { if (b & 1) res = m ? (long long)res * a % m : res * a; a = m ? (long long)a * a % m : a * a; } return res;}// Find a primitive root modulo odd prime power.int primitive_root(PrimePower pp) { std::vector<int> exp; int phi = pp.pe / pp.p * (pp.p - 1); for (auto factor : factorize(pp.p - 1)) { exp.push_back(phi / factor.p); } if (pp.e != 1) exp.push_back(phi / pp.p); int ans = 0; bool succ = false; while (!succ) { ++ans; succ = true; for (int b : exp) { if (pow(ans, b, pp.pe) == 1) { succ = false; break; } } } return ans;}// Euclidean Algorithm.int gcd(int a, int b) { return b ? gcd(b, a % b) : a; }// Extended Euclidean Algorithm.int ex_gcd(int a, int b, int& x, int& y) { if (!b) { x = 1; y = 0; return a; } else { int d = ex_gcd(b, a % b, y, x); y -= a / b * x; return d; }}// Returns the modular inverse of A modulo M.// Assumes that gcd(A, M) = 1, so the inverse exists.int inv(int a, int m) { int x, y; ex_gcd(a, m, x, y); return (x % m + m) % m;}// Find a P-th non-residue mod M.int non_residue(int p, int m, int phi) { std::uniform_int_distribution<int> dis(1, m - 1); while (true) { int c = dis(rng); if (gcd(c, m) == 1 && pow(c, phi / p, m) != 1) return c; } return -1;}// Subroutine: Find a P^E-th root of A mod M.int peth_root_mod_m(int p, int e, int a, int m, int phi) { if (m == 2) return 1; int s = 0, r = phi, pe = pow(p, e); for (; r % p == 0; r /= p, ++s); int q = pe - inv(r, pe); int ans = pow(a, ((long long)q * r + 1) / pe % phi, m); int eta = non_residue(p, m, phi); std::unordered_map<int, int> mp; int zeta = pow(eta, r, m); int xi = pow(eta, phi / p, m); // Precompute powers for BSGS. int B = std::sqrt((s - e) * p + 0.25l) + 1; int pB = pe / B + 1; int po0 = pow(xi, pB, m); for (int j = 1, po1 = 1; j <= B; ++j) { po1 = (long long)po1 * po0 % m; mp[po1] = j; } // Compute p-adic digits of h. for (int j = 0; j < s - e; ++j) { int err = (long long)pow(ans, pe, m) * inv(a, m) % m; int xi_hj = pow(err, pow(p, s - e - j - 1), m); long long hj = 0; // BSGS query. for (int i = 1; i <= pB; ++i) { xi_hj = (long long)xi_hj * xi % m; if (mp.count(xi_hj)) { hj = mp[xi_hj] * pB - i; break; } } ans = (long long)ans * pow(zeta, phi - hj * pow(p, j) % phi, m) % m; } return ans;}// Find a K-th root of A modulo prime P^E.int kth_root_mod_pe(int k, int a, int pe, int phi) { a %= pe; if (k == 0) return a == 1 ? 0 : -1; if (a == 0) return 0; int d = gcd(k, phi); if (pow(a, phi / d, pe) != 1) return -1; a = pow(a, inv(k / d, phi / d), pe); for (int dp = 2; dp * dp <= d && dp * dp * dp * dp < pe; ++dp) { if (d % dp == 0) { int de = 0; for (; d % dp == 0; d /= dp, ++de); a = peth_root_mod_m(dp, de, a, pe, phi); } } if (d != 1) { int dp = gcd(d, phi / d), de = 0; if (dp != 1) { for (; d % dp == 0; d /= dp, ++de); a = peth_root_mod_m(dp, de, a, pe, phi); } if (d != 1) a = peth_root_mod_m(d, 1, a, pe, phi); } return a;}// Subroutine: Find all the K-th roots with a primitive root G known.std::vector<int> calc(int g, int k, int a, int p, int pe) { int mm = p == 2 ? pe / 4 : pe / p * (p - 1); int ans = kth_root_mod_pe(k, a, pe, mm); if (ans == -1) return {}; int d = mm / gcd(k, mm); int po = pow(g, d, pe); std::vector<int> res(mm / d); for (auto& x : res) { x = ans; ans = (long long)ans * po % pe; } return res;}// Find all the K-th roots of A modulo prime power P^E.std::vector<int> kth_roots_mod_pe(int k, int a, PrimePower pp) { int p = pp.p, e = pp.e, pe = pp.pe; a %= pe; if (a == 0) { int d = pow(p, (e - 1) / k + 1); std::vector<int> res(pe / d); for (int i = 0; i < pe / d; ++i) { res[i] = i * d; } return res; } int s = 0; for (; a % p == 0; a /= p, ++s); if (s % k) return {}; int psk = pow(p, s / k), pss = pow(p, s - s / k), pes = pow(p, e - s); std::vector<int> res; if (p != 2) { int g = primitive_root(PrimePower(p, e - s, pes)); res = calc(g, k, a, p, pes); } else if (pes == 2) { res.push_back(a); } else if (k & 1) { int z = a % 4 == 3; a = z ? pes - a : a; res = calc(5, k, a, p, pes); if (z) { for (auto& x : res) x = pes - x; } } else { if (a % 4 == 3) return {}; res = calc(5, k, a, p, pes); int m = res.size(); res.reserve(m * 2); for (int i = 0; i < m; ++i) { res.push_back(pes - res[i]); } } int m = res.size(); res.reserve(m * pss); for (int j = 1; j < pss; ++j) { for (int i = 0; i < m; ++i) { res.push_back(res.end()[-m] + pes); } } for (auto& x : res) x *= psk; return res;}// Find all the K-th roots of A modulo positive integer M.std::vector<int> kth_roots_mod_m(int k, int a, int m) { auto factors = factorize(m); int m0 = 0; std::vector<std::vector<int>> sols; for (const auto& pp : factors) { sols.push_back(kth_roots_mod_pe(k, a, pp)); if (sols.back().empty()) return {}; } std::vector<int> ans; for (int i = 0; i < (int)factors.size(); ++i) { auto pp = factors[i]; if (!i) { m0 = pp.pe; ans = sols[i]; } else { long long m1 = pp.pe * inv(pp.pe, m0); long long m2 = m0 * inv(m0, pp.pe); m0 *= pp.pe; std::vector<int> _ans; _ans.reserve(ans.size() * sols[i].size()); for (auto x : ans) { for (auto y : sols[i]) { _ans.push_back((m1 * x + m2 * y) % m0); } } ans.swap(_ans); } } return ans;}void solve() { int n, m, k; std::cin >> n >> m >> k; auto ans = kth_roots_mod_m(n, k, m); if (ans.empty()) { std::cout << 0 << '\n'; return; } std::cout << ans.size() << '\n'; std::sort(ans.begin(), ans.end()); for (auto x : ans) std::cout << x << ' '; std::cout << '\n';}int main() { int t; std::cin >> t; for (; t; --t) { solve(); }}
实际上,模数 m 未必是素数.只要 a 是模 m 的 k=2e 次本原单位根,就可以用于模 m 的快速数论变换.但是,由于通常需要处理的 2e 比较大,这意味着模数 m 中的每个素因子都是 c2e+1 形式.因此,单个素因子就很大,而模数 m 通常会更大,因而一般模数的情形并没有素数模的情形常用. ↩
根据 原根个数相关结论 可知,λ‑原根的数量恰为 φ(λ(m)),其中,φ(⋅) 和 λ(⋅) 分别是欧拉函数和 Carmichael 函数。因为对于几乎所有整数 m,都有 λ(m)/m=exp(−(1+o(1))loglogmlogloglogm),而存在 C>0,使得对于整数 m>2,都有 φ(m)/m=C/loglogm,所以,对于几乎所有整数 m,都有 φ(λ(m))/m=exp(−(1+o(1))loglogmlogloglogm)。其中,指数部分系数中的 o(1) 吸收了因子 φ(λ(m))/λ(m) 的贡献。故而,λ‑原根可以在期望 exp((1+o(1))loglogmlogloglogm) 次内找到。关于欧拉函数的估计,可以参考论文 Rosser, J. Barkley, and Lowell Schoenfeld. “Approximate formulas for some functions of prime numbers.” Illinois Journal of Mathematics 6, no. 1 (1962): 64-94.关于 Carmichael 函数的估计,可以参考论文 Erdos, Paul, Carl Pomerance, and Eric Schmutz. “Carmichael’s lambda function.” Acta Arith 58, no. 4 (1991): 363-385. ↩
原始论文参见 Adleman, Leonard, Kenneth Manders, and Gary Miller. “On taking roots in finite fields.” In 18th Annual Symposium on Foundations of Computer Science (sfcs 1977), pp. 175-178. IEEE Computer Society, 1977.一个更易读的介绍可见于 Cao, Zhengjun, Qian Sha, and Xiao Fan. “Adleman-Manders-Miller root extraction method revisited.” In International Conference on Information Security and Cryptology, pp. 77-85. Berlin, Heidelberg: Springer Berlin Heidelberg, 2011. ↩
由于这一算法要求 k 是素数,所以最差情形中,它需要对 φ(m) 的最大素因子 p 求 a 模 m 的 p 次方根.这一过程中,需要对 p 次本原单位根求 a 模 m 的离散对数.即使应用 BSGS 算法,这一过程也需要 O(p) 时间.但是,论文 Fouvry, Étienne. “Theoreme de Brun-Titchmarsh; application au theoreme de Fermat.” Inventiones mathematicae 79, no. 2 (1985): 383-407 指出,存在正密度的素数 m,使得 φ(m)=m−1 的最大素因子 p=Ω(m2/3).这意味着这一算法的复杂度至少为 Ω(m1/3),劣于文中介绍的改良 Tonelli–Shanks 算法. ↩