跳转至

高次剩余 & 单位根

前置知识:离散对数

本文讨论模意义下的高次剩余和单位根,并介绍模意义下开方运算的算法.

高次剩余

模运算下的高次剩余,可以认为是在讨论模意义下开高次方的可行性.它是 二次剩余 的推广.

k 次剩余

令整数 k2,整数 a 和正整数 m 互素.若存在整数 x 使得

xka(modm),

则称 a 为模 mk 次剩余k-th residue),xamk 次方根k-th root);否则称 a 为模 mk 次非剩余k-th nonresidue).

也就是说,amk 次方根存在,当且仅当 a 是模 mk 次剩余.

性质

类似二次剩余,可以讨论 k 次剩余的判定、个数以及 k 次剩余类的个数问题.和其他 同余方程 问题一样,可以通过 中国剩余定理 将它们转化为素数幂模的情形.根据原根的有无,这进一步区分为奇素数幂模和模数为 2 的幂次的情形.

奇数幂模的情形较为简单.事实上,对于所有原根存在的情形,都有如下结论:

定理

设整数 k2,整数 a 和正整数 m 互素.设模 m 的原根存在,且 g 是模 m 的一个原根.记 d=gcd(k,φ(m))d=φ(m)d,其中,φ(m)欧拉函数.那么,有:

  1. a 为模 mk 次剩余,当且仅当

    ad1(modm).
  2. a 为模 mk 次剩余时,同余意义下,am 恰有 d 个互不相同的 k 次方根,且它们具有形式

    xgy0+id(modφ(m)), 0y0<d, i=0,1,,d1.
  3. mk 次剩余类的个数为 d,且它们的全体就是

    {gdimodm:0i<d}.
证明

因为 am,所以 xm.因为 g 是模 m 的原根,所以,xa 均与某个 g 的幂次同余.设 xgy(modm),方程 xka(modm) 就等价于

gkygindga(modm).

其中,indga 是离散对数.根据 阶的性质δm(g)=φ(m),这等价于同余方程

kyindga(modφ(m)).

这是关于 y线性同余方程.应用该页面对其解结构的分析,就可以知道方程有解当且仅当 dindga,且通解形式为

y=y0+id(modφ(m)), 0y0<d, i=0,1,,d1.

由此,就几乎可以得到本定理的全部内容;唯一需要额外说明的是判别式 ad1(modm).由 阶的性质 3 可知

δm(a)=δm(gindga)=φ(m)gcd(φ(m),indga)=φ(m)indga.

又已知方程有解当且仅当 dindga,亦即 δm(a)d.由 阶的性质 2 可知,这就等价于该判别式.

模数为 2 的幂次的情形较为特殊.为处理这种情形,需要用到关于模 2e 既约剩余系结构的一个 结论:所有奇数 a 都唯一地同余于某个 (1)s5rmod2e 形式的整数,其中,s{0,1}0r<2e2.借助这一结果,可以得到如下结论:

定理

设整数 k2,奇数 a 和正整数 m=2ee2.那么,当 k 是奇数时,有:

  1. a 恒为模 mk 次剩余.
  2. amk 次方根有且仅有一个.
  3. mk 次剩余类个数为 2e1,且它们就是全体既约剩余类.

k 是偶数时,记 d=gcd(k,2e2)d=2e2d,有:

  1. a 为模 mk 次剩余,当且仅当 a1(mod4)ad1(modm)
  2. a 为模 mk 次剩余时,同余意义下,am 恰有 2d 个互不相同的 k 次方根,且它们具有形式

    x±5y0+id(mod2e1), 0y0<d, i=0,1,,d1.
  3. mk 次剩余类的个数为 d,且它们的全体就是

    {5dimodm:0i<d}.
证明

因为 am,所以 xm.因为 xa 都是奇数,由前述结论可知,可以设 a(1)s5r(mod2e)x=(1)z5y(mod2e).因为表示是唯一的,所以同余方程 xka(mod2e) 等价于 线性同余方程

kzs(mod2),kyr(mod2e2).

结合该页面对于线性同余方程解的分析,就可以得到同余方程 xka(mod2e) 解的结构.根据 k 的奇偶性不同,可以分为两种情形:

  • k 是奇数时,因为 gcd(k,2)=gcd(k,2e2)=1,所以两个线性同余方程对于所有 s,r 都有解,故而原同余方程对于所有奇数 a 总是有解.
  • k 是偶数时,第一个方程有解当且仅当 2s,第二个方程有解当且仅当 d=gcd(k,2e2)r.将两者结合就得到 k 次剩余类的全体形式.直接计算可知,第一个条件等价于 a1(mod4);重复奇素数幂情形的分析可知,第二个条件等价于 ad=1.将两点结合起来就得到定理中的判定方法.两个线性同余方程的通解也是已知的:

    z0,1(mod2),yy0+id(mod2e2), 0y0<2e2.

    将两者结合就得到原方程的通解.

这就完全解决了不同模数下 k 次剩余的判定问题.二次剩余中的 Legendre 记号和二次互反律等内容也可以推广到高次剩余的情形,但这并不容易,需要用到 分圆域 等概念.在代数数论中,二次互反律最终可以推广到 Artin 互反律

单位根

作为 k 次方根的特殊情形,本节讨论 k 次(本原)单位根的概念.它可以看作是复数域 Ck单位根 的概念在模 m 既约剩余系 Zm 中的对应.当模数 m 合适时,用模 mk 次本原单位根代替复数根 ωk 可以加速计算.

类似于复数域的情形,有如下定义:

mk 次单位根

对于模数 m,元素 1k 次方根称为 mk 次单位根k-th root of unity modulo m).特别地,如果 x 是模 m 的一个 k 次单位根,且它不是模 m 的任何 k<k 次单位根,那么,也称 xmk 次本原单位根k-th primitive root of unity modulo m).

比较 原根的定义 可知,原根 g 就是模 mφ(m) 次本原单位根,其中,φ(m)欧拉函数

当模 mk 次本原单位根存在时,它的代数性质和 k 次本原单位复根 ωk 一致,可以代替 ωk 进行各种计算.例如,将它应用于 快速傅里叶变换 中,就得到有限域1上的 快速数论变换

性质

复数域中,任意次(本原)单位根都存在.但是,数论中的(本原)单位根并非如此.

性质

对于模数 m,设 λ(m) 为它的 Carmichael 函数,有:

  1. 所有与 m 互素的整数 a 都是模 mδm(a) 次本原单位根,其中,δm(a)am
  2. 元素 a 是模 mk 次单位根,且 kk 的任意倍数,那么 a 也是模 mk 次单位根.
  3. 元素 a 是模 mk 次(本原)单位根,那么元素 a 是模 mkgcd(k,) 次(本原,相应地)单位根.
  4. k 遍历 k 的因数,所有模 mk 次本原单位根恰构成模 mk 次单位根的一个划分.而且,对于 k,映射 xx 给出 k 次单位根之间的双射,且保持上述划分不变:它将 kk 次本原单位根仍然映射到 k 次本原单位根.
  5. mk 次本原单位根存在,当且仅当 kλ(m).特别地,模 mλ(m) 次本原单位根存在,称为 mλ‑原根
  6. 元素 a 是模 mk 次单位根,当且仅当 ak1(modm) 且对于任意素因子 pk 都有 ak/p1(modm)
证明

根据阶的定义,所有与 m 互素的整数 a 都是模 mδm(a) 次本原单位根,其中,δm(a)am 的阶.反过来,如果 a 是模 mk 次单位根,那么 gcd(ak,m)=1,所以 gcd(a,m)=1.因此,a 是模 m 的(本原)单位根,当且仅当 am 互素.这就是性质 1.

直接验证定义可知,只要 kk,就可以从 ak1(modm) 推出 ak1(modm),这就是性质 2.根据 阶的性质 可知

δ(a)=δm(a)gcd(δm(a),).

如果 a 是模 mk 次本原单位根,那么,δm(a)=k,直接代入上式就得到 a 是模 mkgcd(k,) 次本原单位根.如果 a 只是模 mk 次单位根,设它是 kk 次本原单位根,故而 a 是模 mkgcd(k,) 次本原单位根.由于 kk,有

kgcd(k,)kgcd(k,),

再由性质 2,就得到 a 是模 mkgcd(k,) 次单位根.这就是性质 3.

对于 kk,由性质 2,模 mk 次本原单位根必然是模 mk 次单位根.它们两两不交,故而构成划分.而对于 k,总有 k,因此对于模 mk 次本原单位根 a,总有 a 是模 mk 次本原单位根.取 =1modk,可以验证 xxxx 互为逆映射,因此,xx 是双射.这就是性质 4.

根据 Carmichael 函数的性质可知,模 mλ(m) 次本原单位根总是存在的,设它为 a,且 δm(a)=λ(m).对于 kλ(m),设 k=λ(m)k,总有

δm(ak)=λ(m)(λ(m),k)=λ(m)k=k.

因此,akk 次本原单位根.而根据 Carmichael 函数的定义,所有 xm 的阶都是 λ(m) 的因子.这就得到性质 5.

几乎重复 原根判定定理 的证明,就可以得到性质 6.这一判别方法实际上在验证 δm(a)=k

从这些性质可以看出,相对于原根存在的情形,模 mλ‑原根起到了类似的基础作用.与原根不同的是,λ‑原根的幂次并不能用于生成模 m 的全体单位根.尽管如此,由于 λ‑原根的密度并不低2,如果确实需要找到 k 次本原单位根,可以首先通过随机方法找到一个 λ‑原根,再通过求幂次得到一个 k 次本原单位根.

如果已知 am 的一个 k 次方根,可以通过模 m 的全体 k 次单位根生成 am 的全体 k 次方根.

定理

xam 的一个 k 次方根,当 r 遍历模 m 的全体 k 次单位根时,xr 遍历 am 的全体 k 次方根.

证明

对于 am 的两个 k 次方根 x,y,设 r=x1ymodm,那么 r 满足 rk1(modm),是模 mk 次方根.反过来,只要 r 是模 mk 次单位根,那么,(xr)k=xkrka(modm),也就是说,xr 是模 mk 次方根.

利用 k 次单位根生成全体 k 次方根,就类似于利用齐次线性方程组的解生成非齐次线性方程组的通解一样.

前面讨论的是一般情形.仅对于原根存在的情形,单位根的结构更为简单:

定理

对于模数 m,设模 m 的原根存在,且 a 是模 mk 次本原单位根.那么,b 是模 mk 次单位根,当且仅当它可以表示为 a 的幂次.

证明

g 是模 m 的原根,那么,所有与 m 互素的元素都可以表示为 g 的幂次.那么,a 是模 mk 次本原单位根,当且仅当

δm(a)=δm(gindga)=φ(m)gcd(φ(m),indga)=k.

类似地,b 是模 mk 次单位根,当且仅当

δm(b)=δm(gindgb)=φ(m)gcd(φ(m),indgb)=kk.

所以,有

gcd(φ(m),indga)gcd(φ(m),indgb)indgb.

根据对线性同余方程的 分析 可知,这一条件就等价于方程

(indga)xindgb(modφ(m))

有解.将这一条件对 g 取幂,就得到 axb(modm),亦即 b 可以表示为 a 的幂次.

这一定理说明,原根存在时,全体 k 次单位根呈现 循环群 的结构,而 k 次本原单位根则是该循环群的生成元.稍后将会看到,Tonelli–Shanks 算法正是利用这一点,加速了开方运算中离散对数部分的计算.

模意义下开方

最后,本文讨论 k 次方根的求法.对于 k=2 的情形,有 很多高效算法 可以用于模意义下开平方运算.但是,对于一般的 k,并没有已知的多项式时间算法.本节将介绍两种常见算法,分别可以在 O(m1/2)O(m1/4+ε) 时间内求出一个 k 次方根.利用中国剩余定理总是可以将问题转换为素数幂模的情形,因此,本节主要讨论素数幂模情形的解法.

朴素算法

前文对于 k 次剩余性质的 分析 实际上已经指出了一种求解素数幂模下 k 次方根的方法.严格来说,前文解决的情形是被开方数 a 与模数 m 互素的情形.算法过程总结如下:

  • m=pe 是奇素数幂时,设模 m 的一个原根是 g.那么,方程 xka(modm) 可以转化为线性同余方程

    kyindga(modφ(m)).

    其中,indga 可以通过 BSGS 算法 求出,而 线性同余方程 的全体解容易求出.由此,就可以得到 a 的全部 k 次方根 xgy(modm)

    除此之外,还有另一种相仿的思路.同样是设 xgy(modm),还可以通过变形

    xk(gk)ya(modm)

    转化为求底数为 gka 的离散对数.这同样可以通过 BSGS 算法找到一组特解.它的通解可以通过前文的解的表达式求出,也就是将特解与全体 k 次单位根逐一相乘得到.

    无论采用哪种思路,原根已知时,该算法求出单个解的复杂度都是 O(m1/2).因为可以在 o(m1/2) 时间内找到一个原根,所以,总的时间复杂度仍然是 O(m1/2)

  • m=2eeN+ 时,可以首先求出 a(1)s5r(modm) 中的 s,r.这两个指数中,s 可以在 O(1) 时间内确定:

    s={0,a1(mod4),1,a3(mod4).

    r=ind5((1)sa) 可以通过 BSGS 算法在 O(m1/2) 时间内求出.接下来,只需要求解线性同余方程组:

    kzs(mod2),kyr(mod2e2).

    这个线性方程组的通解 (z,y) 容易求出,而 x=(1)z5y 就是所求的方根.这一算法求出单个解的复杂度仍然是 O(m1/2)

当然,对于无解的情形,其实可以通过前文叙述的判别方法在 O(logm) 时间内快速判断,而无需在求解过程中判断.

求素数模 k 次方根的参考实现如下:(代码仅作示意,由于时间复杂度过高,无法通过本题)

模板题 Library Checker - Kth Root (Mod) 参考实现
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// 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 算法

将用于模意义下开平方的 Tonelli–Shanks 算法 做适当推广,就可以解决素数幂模下开方运算.一种较为直接的推广方式是 Adleman–Manders–Miller 算法3,但是它的复杂度仍然不够优秀4.本节介绍由 sugarknri、Min_25、37zigen 等人提出的改良 Tonelli–Shanks 算法.它可以在 O(m1/4+ε) 时间内求出一个 k 次方根.

Tonelli–Shanks 算法的核心想法是,将离散对数的求解放到阶为 2e 的群里,进而降低时间复杂度.类似地,对于任意素数幂 pe 阶群内的离散对数,同样可以较为高效地求解,但是算法的复杂度为 Ω(p).Adleman–Manders–Miller 算法将 k 次方根的求解分拆为多个素数幂阶群内离散对数的计算,但是受限于 k 的最大素因子 pmax(k) 的大小,算法复杂度仍然为 Ω(pmax(k)).本节算法进一步改良了这一过程,避免了对较大的素因子计算离散对数,进而将整体复杂度控制到 O(m1/4+ε)

过程

考虑素数幂模 mak 次方根的计算,即求解同余方程:

xka(modm).

特别地,对于 m=2e 的情形,还需要保证 a1(mod4),进而 a 可以写成 g=5 的幂次.类似前文讨论,模 2ek 次方根的计算总是可以转化为这样的情形.处理模 2e 的情形时,本节提到的 φ(m) 都应换作 δm(5)=2e2

首先,问题可以转化为开方次数整除 φ(m) 的情形.设 d=gcd(k,φ(m)).那么,由 k 次剩余的性质可知,当 amk 次剩余时,a 总是模 mφ(m)d 次单位根.根据单位根的性质,对于任意 φ(m)d,映射 xx 都是 φ(m)d 次单位根之间的双射.因此,可以取

=(kd)1modφ(m)d.

将原来的同余方程两侧同时取 次幂,就得到

xdxka=:b(modm).

最左侧同余号利用了 欧拉定理 和如下同余关系:(cZ

k=d(kd)=d(cφ(m)d+1)d(modφ(m)).

对于转化后的问题,考虑 d 的素因数分解:

d=pPpe.

可以从 b=a 开始,对每个 pe1,依次开 pe 次方,最后就能得到 bd 次方根,也就是 ak 次方根.

最后,问题转化为如何求如下方程的解:

xpeb(modm).

不妨设 φ(m)=psrpr.设 qN+ 是方程 qr1(modpe) 的解.那么,因为 brpse 次单位根,所以 bqr 一定是 pse 次单位根.又设 ζ 是模 mps 次本原单位根.那么,ζpepse 次本原单位根,进而存在 hN 使得 bqrζhpe(modm).所以,直接验证可知

xb(qr+1)/peζh(modm)

bmpe 次方根.

为了计算 x,需要找到模 m 的一个 p 次非剩余 η.为此,由前文性质,只需要随机 ηm 并验证 ηφ(m)/pmodm1 即可.这样的数的密度是

φ(m)m(11p)14.

因此,期望随机不超过 4 个整数就能找到它.注意到,ηrps11(modm)ηrps1(modm),所以,如果设 ζ=ηrmodmξ=ηrps1modm,那么它们分别是 ps 次和 p 次本原单位根.

最后,需要计算 hN.显然,可以取 h<pse.考虑 hp 进制表示:

h=j=0se1hjpj=h0+h1p+h2p2+.

逐位计算这些数位.当前 j 个数位都计算完成时,必然有

(bqrζpe(h0+h1p++hj1pj1))psej1ζhjps1ξhj(modm).

故而,hj 可以通过计算关于 ξ 的离散对数求出.为了获得更好的时间复杂度,需要使用 BSGS 算法.总共需要计算 (se) 次离散对数,设预处理 Bξ 的幂次,则单次求解离散对数的时间复杂度为 O(p/B),总的时间复杂度为

O(B+(se)pB).

B=(se)p 时,总的时间复杂度最低,为 O((se)p).得到 h 之后,代入前文 x 的表达式,就可以找到一个特解.

时间复杂度

这一算法的时间复杂度为 O(m1/4+ε).本节讨论复杂度时,总是假设单次乘法需要 O(1) 时间,且计算幂次时,总是应用欧拉定理降幂,则涉及的单个幂的计算总是可以在 O(logm) 时间内完成.

先考虑单个 pe 次方根的计算.找到 p 次非剩余只需要验证期望 O(1) 个数,总时间复杂度为 O(logm).计算 s,r,ζ,η,bqr 各只需要 O(logm) 时间.计算 h 时,单个数位需要通过 O(logm) 时间计算幂次,总共 (se) 位,故而总的时间复杂度为 O((se)logm).前文已经说明,计算离散对数的部分预处理和 (se) 次查询的总时间为 O((se)p).因为 seO(logm),所以单个 pe 次方根的计算的时间复杂度为 O(p1/2+ε).特别地,当 s=e 时,时间复杂度可以进一步减少为 O(logm)

进而,可以考虑算法总的时间复杂度.计算 φ(m),d, 的时间复杂度均为 O(logm).紧接着需要做素因数分解 d=ppe,这一步利用 Pollard Rho 算法 可以在 O(m1/4) 时间内完成.最后,依次求 pe 次方根的总时间复杂度为

O(e<sp1/2+ε).

由于满足 e<s 的素因子 p 至少在 φ(m) 中出现 2 次,必然有 p<m1/2.故而,总时间复杂度为 O(m1/4+ε)

事实上,在这一情景中,无需使用 Phollard Rho 算法分解素因数,仍然可以获得 O(m1/4+ε) 的时间复杂度.事实上,只需要对 d 暴力试除进行分解,并只枚举到不超过 m1/4 的素因子.设去除这些小素因子后得到的整数为 z.那么,对于 z 的素因子 p>m1/4,必然有 νp(φ(m))<4,其中,νp(n) 表示 n 的素因数分解中 p 的次数.由于只需要考虑

1e=νp(d)<s=νp(φ(m))<4

的情形,满足该条件的素因子 p 至多只能有一个;否则,它们在 φ(m) 中的次数都不小于 2,总的乘积必然超过 m.要分离出这个(可能存在的)唯一的大素因子,只需要计算

p=gcd(z,φ(m)z)=p:νp(d)<νp(φ(m))pmin{νp(d),νp(φ(m))νp(d)}.

枚举 νp(d),νp(φ(m)) 的所有可能性可知,乘积中 p 的次数一定是 1,因此这样算出来的就是唯一的大素因子 p(如果存在的话).至于剩余的部分 z/p,因为其中只能包含若干满足 e=s 的素因子,所以无需继续分解.

求素数模 k 次方根的参考实现如下:

模板题 Library Checker - Kth Root (Mod) 参考实现
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#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.如果 a0(modm),那么

x=pe/k(modpe), =0,1,,pee/k1

都是原方程的解.接下来,考察 a0(modm) 的情形.设 a=psapa.于是,设 x=pzxpx,就有

xk=pkz(x)kpsa(modpe).

由于 (x)kp,所以该式成立当且仅当 kz=s(x)ka(modpes).当且仅当 ks 时,第一个方程有解 z=sk;而第二个方程的求解已经解决.需要注意的是,因为第二个方程的通解的模数与原方程通解的模数并不相同,所以第二个方程的每一个解 x,都对应原方程的若干解:

xps/k(x+pes)(modpe), =0,1,,pss/k1.

求解任一模数下全体 k 次方根的参考实现如下:

模板题 Luogu P5668【模板】N 次剩余 参考代码
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
#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();
  }
}
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
#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();
  }
}

参考资料与注释


  1. 实际上,模数 m 未必是素数.只要 a 是模 mk=2e 次本原单位根,就可以用于模 m 的快速数论变换.但是,由于通常需要处理的 2e 比较大,这意味着模数 m 中的每个素因子都是 c2e+1 形式.因此,单个素因子就很大,而模数 m 通常会更大,因而一般模数的情形并没有素数模的情形常用. 

  2. 根据 原根个数相关结论 可知,λ‑原根的数量恰为 φ(λ(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. 

  3. 原始论文参见 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. 

  4. 由于这一算法要求 k 是素数,所以最差情形中,它需要对 φ(m) 的最大素因子 pamp 次方根.这一过程中,需要对 p 次本原单位根求 am 的离散对数.即使应用 BSGS 算法,这一过程也需要 O(p) 时间.但是,论文 Fouvry, Étienne. "Theoreme de Brun-Titchmarsh; application au theoreme de Fermat." Inventiones mathematicae 79, no. 2 (1985): 383-407 指出,存在正密度的素数 m,使得 φ(m)=m1 的最大素因子 p=Ω(m2/3).这意味着这一算法的复杂度至少为 Ω(m1/3),劣于文中介绍的改良 Tonelli–Shanks 算法.