算法学习笔记(11): 原根
原根
此文相对困难,请读者酌情食用
在定义原根之前,我们先定义其他的一点东西
阶
通俗一点来说,对于 在模 意义下的阶就是 的最小正整数解
或者说, 在模 意义下生成子群的阶(群的大小)
再或者说,是 在模 意义下的循环节的大小
循环节,生成子群……真绕……其实两者很类似
两者的大小也就是在模 意义下集合 不重复元素的个数
可以通过欧拉定理可知 也就是说存在一个数 ,使得
所以,就有了循环……
我们将 在模 意义下的阶记作
显然,无论是根据群论还是什么, 一定是 的因数
特别的,当 时,称 为模 意义下的一个原根
那么原根的定义出来了……
原根
上述定义或许不是那么简单,我们换一种说法
满足上述两个条件的数 就是模 意义下的一个原根
这两个条件也是我们在程序中验证原根的方法 QwQ
而对于原根来说, 在 下各不相同,他们就是最短的循环节,也是模 意义下的完全剩余系,或者说原根的生成子群,而这也是原根最重要的性质。
但是,并不是每一个数都存在原根
例如 ,但是没有任何数在模 下的阶为
为判断一个数是否有原根,我们有一个重要的定理:
正整数 有原根的充要条件为 能表示成 中的任何形式之一,其中 为奇素数
由于证明比较复杂,若感兴趣可以参见这篇博客 原根证明
那么如何求出一个数 有多少个原根
假设我们已经求出了一个原根
由于原根一定存在与 的完全剩余系中,而 的生成子群与之等价,也就是说,我们需要在
这个集合中寻找所有的原根
所以,考虑构造出判断阶的方法。我们令
那么
那么 也就是 的阶
若需要满足原根的定义,我们必须使得
同时考虑 ,,也就是说有总共有 个原根
这同时启发我们,只要我们找到了任意一个模 意义下的原根,我们就可以求出所有原根。
至于如何找到原根,选择暴力枚举即可
有证明:如果一个数 有原根,则其最小原根在渐近意义下不大于 级别,所以直接枚举是没有任何问题的
那么我们总结一下求 的原根的所有步骤
-
预处理
-
利用线性筛求出所有的质数以及每一个数的 值
-
对每一个筛出的质数,标记出所有 和 (别忘了)
-
-
判断 是否有原根
-
求最小原根
-
求出 的所有质因数 ,记录 (记得将 也记录进去)
-
枚举一个数 ,对于每一个记录的数 分别计算 ,如果 ,说明 不是原根 (这里再下文中有解释)
-
重复第二部,直到找到一个原根 为止
-
-
求所有原根
-
枚举
-
如果 ,则 是一个原根,记录下他
-
解释:求最小原根的判断
为什么我们只需要枚举 就行了?
考虑我们将 分解成
由于如果需要满足 ,一定有
那么对于 ,一定有
也就是说,所有 就包含了所有情况了
那么为什么要加上 呢,这就交给读者消化思考喽 _
参考代码如下:真的只供参考,这种写法特别慢
template<typename T>
inline T gcd(T x, T y) {
T z;
while (y) z = x % y, x = y, y = z;
return x;
}
template<typename T>
inline T qpow(T a, T x, T p) {
T r(1); a %= p;
while (x) {
if (x & 1) r = (r * a) % p;
a = (a * a) % p, x >>= 1;
}
return r;
}
int phi[N], notp[N];
std::vector<int> prm;
void getPrm() {
phi[1] = 1;
for (int i = 2; i < N; ++i) {
if (!notp[i]) prm.push_back(i), phi[i] = i - 1;
for (const int &j : prm) {
if (i * j >= N) break;
notp[i * j] = true;
if (i % j == 0) {
// i | n && i^2 | n => phi(n) = phi(n / i) * i
phi[i * j] = phi[i] * j; break;
} else {
// i | n && not i^2 | n => phi(n) = phi(n / i) * (i - 1)
phi[i * j] = phi[i] * phi[j];
}
}
} // end getPrm for
}
bool exists[N];
void getExists() {
exists[2] = exists[4] = true;
for (int i = 1; i < (int)prm.size(); ++i) {
int p = prm[i];
for (int q = p; q < N; q *= p) {
exists[q] = true;
if (q * 2 < N) exists[q * 2] = true;
}
}
// printf("Exists init!\n");
}
void factorize(int x, vector<int> & v) {
v.push_back(1);
for (const int &p : prm) {
if (p >= x) break;
if (x % p == 0) v.push_back(x / p);
}
}
void getAll(int p, vector<int> & v) {
// no answer
if (!exists[p]) return;
int ph = phi[p];
int fst, cur;
vector<int> factors; factors.clear();
factorize(ph, factors);
// enum i which gcd(i, m) == 1
// find first element i suit i^ph = 1 mod p
for (int i = 1; ; ++i) {
if (gcd(i, p) != 1) continue;
// if (qpow(i, ph, p) != 1) continue;
bool valid = true;
// we need i only if i^ph = 1 mod p, not other numbers.
for (auto &e : factors) {
if (e != ph && qpow(i, e, p) == 1) {
valid = false; break;
}
}
if (valid) {
fst = cur = i; break;
}
}
for (int i(1); i <= ph; ++i) {
if (gcd(i, ph) == 1) v.push_back(cur);
cur = cur * fst % p;
}
}
考虑模板可能有爆
int
的风险,请参考者合理使用long long
这样通过
getAll
得出的vector
是乱序的,需要再排序一次
求解快速幂
对于一个比较小的模数 ,可以通过 的预处理,然后 的回答 。
找到原根 是 naive
的,预处理 是 naive
的,所以预处理出 也是 naive
的。
发现 ,于是我们只需要算 即可。
由于 和 都预处理过了,所以可以 算。
将模意义下乘法转换为对于指数的加法
例如 就利用了这一点,将复杂度变为了 。
其他小东西
- ,也就是 有二次剩余的(充要)条件为 ,考虑 在 下没有逆元,因为 。例如
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?