原根
根据欧拉定理我们知道,若 互质,
。因此,
这样一个数列在模
意义下将有一个
长度的循环节,因为
又回到了一开始的
。
然而,我们并不能保证它是最短的循环节,例如, ,可以发现这时最短循环节长度为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[