原根
根据欧拉定理我们知道,若 互质,
。因此,
这样一个数列在模
意义下将有一个
长度的循环节,因为
又回到了一开始的
。
然而,我们并不能保证它是最短的循环节,例如, ,可以发现这时最短循环节长度为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[m], fst;
auto factors = get_factors(phiM);
for (int i = 1;; ++i)
{
if (gcd(i, m) != 1)
continue;
bool ok = true;
for (auto e : factors)
if (e != phiM && qpow(i, e, m) == 1)
{
ok = false;
break;
}
if (ok)
{
fst = i;
break;
}
}
int cur = fst;
for (int i = 1; i <= phiM; ++i)
{
if (gcd(phiM, i) == 1)
v.push_back(cur);
cur = cur * fst % m;
}
return v;
}
当然,这样求出来的vector
很可能是乱序的,常常还需要排序操作。
此外,如果只需要求最小原根,可以用下面的占用空间较小的算法:
int get_minimum_primitive_root(int m)
{
int phiM = phi(m);
for (int i = 1;; ++i)
{
if (__gcd(i, m) != 1)
continue;
auto factors = get_factors(phiM);
bool ok = true;
for (auto e : factors)
if (e != phiM && qpow(i, e, m) == 1)
{
ok = false;
break;
}
if (ok)
return i;
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
2020-12-09 cgo
2020-12-09 go bug