算法学习笔记(11): 原根

原根

此文相对困难,请读者酌情食用

在定义原根之前,我们先定义其他的一点东西


通俗一点来说,对于 a 在模 p 意义下的就是 ax1(modp)最小正整数解 x

或者说,a 在模 p 意义下生成子群的阶(群的大小)

再或者说,是 a 在模 p 意义下的循环节的大小

循环节,生成子群……真绕……其实两者很类似

两者的大小也就是在模 p 意义下集合 {a1,a2,a3,,ak} 不重复元素的个数

可以通过欧拉定理可知 aφ(p)a01(modp) 也就是说存在一个数 k,使得 aka(modp)

所以,就有了循环……

我们将 a 在模 m 意义下的记作 ordma

显然,无论是根据群论还是什么,ordma 一定是 φ(p) 的因数

特别的,当 ordma=φ(p) 时,称 a 为模 p 意义下的一个原根

那么原根的定义出来了……


原根

上述定义或许不是那么简单,我们换一种说法

x[1,φ(p)),ax1(modp)aφ(p)1(modp)

满足上述两个条件的数 a 就是模 p 意义下的一个原根

这两个条件也是我们在程序中验证原根的方法 QwQ

而对于原根来说,a1,a2,,aφ(p)m 下各不相同,他们就是最短的循环节,也是模 p 意义下的完全剩余系,或者说原根的生成子群,而这也是原根最重要的性质。


但是,并不是每一个数都存在原根

例如 φ(8)=4,但是没有任何数在模 8 下的阶为 4

为判断一个数是否有原根,我们有一个重要的定理:

正整数 k 有原根的充要条件为 k 能表示成 2,4,pn,2pn 中的任何形式之一,其中 p为奇素数

由于证明比较复杂,若感兴趣可以参见这篇博客 原根证明


那么如何求出一个数 p 有多少个原根

假设我们已经求出了一个原根 g

由于原根一定存在与 p 的完全剩余系中,而 g 的生成子群与之等价,也就是说,我们需要在

{gk|k[1,φ(p))}

这个集合中寻找所有的原根

所以,考虑构造出判断阶的方法。我们令 d=gcd(k,φ(p))

那么

gkφ(p)dgφ(p)kd1kd1(modp)

那么 kgcd(k,φ(p)) 也就是 gk 的阶

若需要满足原根的定义,我们必须使得 gcd(k,φ(p))=1

同时考虑 g=g1gcd(1,φ(p))=1,也就是说有总共有 φ(φ(p)) 个原根


这同时启发我们,只要我们找到了任意一个模 p 意义下的原根,我们就可以求出所有原根。

至于如何找到原根,选择暴力枚举即可

有证明:如果一个数 n 有原根,则其最小原根渐近意义下不大于 n4 级别,所以直接枚举是没有任何问题的


那么我们总结一下求 n 的原根的所有步骤

  • 预处理

    • 利用线性筛求出所有的质数以及每一个数的 φ

    • 对每一个筛出的质数,标记出所有 pq2pq (别忘了2,4)

  • 判断 n 是否有原根

  • 最小原根

    • 求出 φ(m) 的所有质因数 i,记录 m/i (记得将 1 也记录进去)

    • 枚举一个数 g,对于每一个记录的数 j=m/i 分别计算 gj,如果ij1(modm) ,说明 i 不是原根 (这里再下文中有解释)

    • 重复第二部,直到找到一个原根 g 为止

  • 所有原根

    • 枚举 k[1,φ(m))

    • 如果 gcd(k,φ(m))=1,则 gk 一个原根,记录下他

解释:求最小原根的判断

为什么我们只需要枚举 m/i 就行了?

考虑我们将 m 分解成 m=i1a1i2a2ikak

由于如果需要满足 ij1(modp),一定有 j|φ(p)

那么对于 m/i=kj,一定有 im/i1modp

也就是说,所有 m/i 就包含了所有情况了

那么为什么要加上 1 呢,这就交给读者消化思考喽 _

参考代码如下:真的只供参考,这种写法特别慢

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 是乱序的,需要再排序一次

O(1) 求解快速幂

对于一个比较小的模数 p1e7,可以通过 O(p) 的预处理,然后 O(1) 的回答 xk

找到原根 gnaive 的,预处理 gknaive 的,所以预处理出 loggx 也是 naive 的。

发现 loggxk=kloggx,于是我们只需要算 gkloggx 即可。

由于 gkloggx 都预处理过了,所以可以 O(1) 算。

将模意义下乘法转换为对于指数的加法

例如 2024.01.15dance 就利用了这一点,将复杂度变为了 O(p2w)

其他小东西

  • p1gφ(p)2,也就是 p1 有二次剩余的(充要)条件为 4φ(p),考虑 2modφ(p) 下没有逆元,因为 gcd(2,φ(p))=2。例如 2024.01.16coins
posted @   jeefy  阅读(302)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示