原根

根据欧拉定理我们知道,若 [公式] 互质,[公式] 。因此, [公式] 这样一个数列在模 [公式] 意义下将有一个 [公式] 长度的循环节,因为 [公式] 又回到了一开始的 [公式] 。

然而,我们并不能保证它是最短的循环节,例如, [公式] ,可以发现这时最短循环节长度为3,而不是 [公式] 。我们把这个最短循环节的长度定义为 [公式] 在模 [公式] 下的阶,记作 [公式] 。严格地,定义 [公式] 在模 [公式] 下的阶是同余方程 [公式] 的最小正整数解。

显然,[公式] 一定是 [公式] 的因数。特别地,当 [公式] 时,就称 [公式] 为模 [公式] 下的一个原根。对于原根 [公式] 来说, [公式] 在模 [公式] 下各不相同,它们就是最短的循环节。

并不是每个数都存在原根,例如 [公式] ,但没有任何数在模8下的阶为4。为判断一个数是否有原根,我们有一个重要的定理:

正整数有原根的充要条件为:它能表示为下列形式之一: [公式] ,其中 [公式] 为奇素数。

(证明比较复杂,若感兴趣可参见这篇博客

那么,如何判断一个数有多少原根?可以发现,如果 [公式] 是模 [公式] 的原根,那么对于任意和 [公式] 互质的正整数 [公式] ,[公式] 也是模 [公式] 的原根(此时 [公式] 在模 [公式] 下互不相同,于是[公式] 在模 [公式] 下互不相同)。容易证明,它们就是 [公式] 的全部原根(注意到模 [公式] 下与 [公式] 互质的数一共只有 [公式] 个,已经被 [公式] 占据完了)。所以原根的数量就是模 [公式] 意义下 [公式] 的数量,即 [公式] 。

这同时启示我们,求原根时,只需要找到一个原根,就很容易得到全部原根。至于如何找到原根,暴力枚举即可。因为数学家已经证明,一个数 [公式] 若有原根,则其最小原根在渐进意义下不大于 [公式] 级别[1],所以直接枚举的时间复杂度是比较低的。


求 [公式] 的所有原根的步骤为:

  1. 预处理
    1. 线性筛出不大于 [公式] 的素数,并求出所有不大于 [公式] 的正整数的欧拉函数值。
    2. 对每个不大于 [公式] 的素数 [公式] ,求出所有不大于 [公式] 的 [公式] 和 [公式] 。
  2. 判断 [公式] 是否有原根
  3. 求最小原根
    1. 求出 [公式] 的所有因数
    2. 枚举与 [公式] 互质的 [公式]
    3. 对于 [公式] 的每个因数 [公式] ,分别计算 [公式] ,如果 [公式] 但 [公式] ,说明 [公式]不是原根[2]
    4. 继续循环,直到找到合适的 [公式] 为止
  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;
    }
}

 

参考

  1. ^准确地说,设g为p的最小原根,则对任意ε>0,都存在C>0使得g≤Cp^(1/4+ε)
  2. ^只需要枚举因数,是因为阶必然是phi(m)的因数
posted @ 2021-12-09 18:40  zJanly  阅读(275)  评论(0编辑  收藏  举报