原根
根据欧拉定理我们知道,若 互质, 。因此, 这样一个数列在模 意义下将有一个 长度的循环节,因为 又回到了一开始的 。
然而,我们并不能保证它是最短的循环节,例如, ,可以发现这时最短循环节长度为3,而不是 。我们把这个最短循环节的长度定义为 在模 下的阶,记作 。严格地,定义 在模 下的阶是同余方程 的最小正整数解。
显然, 一定是 的因数。特别地,当 时,就称 为模 下的一个原根。对于原根 来说, 在模 下各不相同,它们就是最短的循环节。
并不是每个数都存在原根,例如 ,但没有任何数在模8下的阶为4。为判断一个数是否有原根,我们有一个重要的定理:
正整数有原根的充要条件为:它能表示为下列形式之一: ,其中 为奇素数。
(证明比较复杂,若感兴趣可参见这篇博客)
那么,如何判断一个数有多少原根?可以发现,如果 是模 的原根,那么对于任意和 互质的正整数 , 也是模 的原根(此时 在模 下互不相同,于是 在模 下互不相同)。容易证明,它们就是 的全部原根(注意到模 下与 互质的数一共只有 个,已经被 占据完了)。所以原根的数量就是模 意义下 的数量,即 。
这同时启示我们,求原根时,只需要找到一个原根,就很容易得到全部原根。至于如何找到原根,暴力枚举即可。因为数学家已经证明,一个数 若有原根,则其最小原根在渐进意义下不大于 级别[1],所以直接枚举的时间复杂度是比较低的。
求 的所有原根的步骤为:
- 预处理
- 判断 是否有原根
- 求最小原根
- 求出 的所有因数
- 枚举与 互质的
- 对于 的每个因数 ,分别计算 ,如果 但 ,说明 不是原根[2]
- 继续循环,直到找到合适的 为止
- 求所有原根
- 枚举 以内的正整数
- 如果 与 互质,则 是一个原根
主要代码如下:
int phi[MAXN]; // 欧拉函数表
bool isnp[MAXN]; // 是否不是素数
vector<int> primes; // 质数表
int qpow(int a, int n, int p); // 快速幂
void init(int n); // 欧拉筛
// 实现省略,可参照之前的笔记
vector<int> get_factors(int a) // 求所有因数
{
vector<int> v;
for (int i = 1; i * i <= a; ++i)
if (a % i == 0)
{
v.push_back(i);
if (i * i != a) v.push_back(a / i);
}
return v;
}
bool exist[MAXN]; // 是否存在原根
void init_exist() // 初始化exist数组
{
exist[2] = exist[4] = true;
for (int i = 1; i < (int)primes.size(); ++i)
{
int p = primes[i];
for (int q = p; q < MAXN; q *= p)
{
exist[q] = true;
if (q * 2 < MAXN)
exist[q * 2] = true;
}
}
}
vector<int> get_primative_roots(int m) // 求所有原根
{
vector<int> v;
if (!exist[m])
return v;
int phiM = phi[