数论取模学习笔记

· · 算法·理论

前言:本文是作者在对同学讲解完之后有感而发写下的,有错误望指正。

费马小定理

对于一个质数 p 及一个任意满足 (a,p)=1a,我们有:

a^{p-1}\equiv1\pmod p

证明

首先我们关注两个引理。

引理一:模的除法

对于任意 mc 满足 (m,c)=1,若

a\times c\equiv b\times c\pmod m

a\equiv b\pmod m

事实上,对于任意的

a\times c\equiv b\times c\pmod k

都有

a\equiv b\pmod{m / (m,c)}

可以试着证明一下,此处不再赘述。

引理二:完全剩余系

::::info[完全剩余系] 通俗来讲,模 m 的完全剩余系就是一个有 m 个元素的集合,其中任意两个数模 m 的余数都不相等。 :::: 如果 \{a_0,a_1,\cdots,a_{m-1}\} 是模 m 的完全剩余系,那么对于满足 (b,m)=1b\{b\times a_0,b\times a_1,\cdots,b\times a_{m-1}\} 也是模 m 的完全剩余系。

证明:

运用反证法。

假设新的集合不是模 m 的完全剩余系,那么存在 b\times a_ib\times a_j 使得:

b\times a_i\equiv b\times a_j\pmod m

那么由于引理一,有:

a_i\equiv a_j\pmod m

那么一开始的集合就也不是模 m 的完全剩余系,与已知条件矛盾。

所以新集合是模 m 的完全剩余系。

费马小定理的证明

易得 \{0,1,2,\cdots,p-1\} 是一个模 p 的完全剩余系。

由于 (a,p)=1,根据引理二,\{0,a,2a,\cdots,(p-1)a\} 也是模 p 的完全剩余系。

我们把两个集合的所有元素(除了 0)全部乘起来再取余,容易得到:

1\times2\times\cdots\times(p-1)\equiv a\times2a\times\cdots\times(p-1)a\pmod p

(p-1)!\equiv(p-1)!\times a^{p-1}\pmod p

由于 p 是质数,明显 (p-1)!p 互质。

所以根据引理一,得到

a^{p-1}\equiv1\pmod p

得证。

费马小定理的应用

求乘法逆元是数论中一个很重要的板块。具体来讲,就是要求满足 a\times b\equiv1\pmod mb,此时一般把 b 写作 a^{-1}

那么对于一个质数 p,对于与它互质的 a,有:

a^{p-1}\equiv1\pmod p\\ a\times a^{p-2}\equiv1\pmod p

所以:

a^{p-2}\equiv a^{-1}\pmod p

这样再加上一个快速幂模板,我们就可以 O(\log p) 求逆元了。

代码如下。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

ll a, p;

inline ll quick_power(ll a, ll b, ll mod) { // a 的 b 次方对 mod 取模
    ll res = 1;
    while (b) {
        if (b & 1)
            res = res * a % mod;
        a = a * a % mod;
        b >>= 1;
    }
    return res;
}

int main() {
    scanf("%lld%lld", &a, &p);
    ll ins = quick_power(a, p - 2, p); // 求逆元
    printf("%lld\n", ins);
}

欧拉定理

对于两个数 a,m,如果 (a,m)=1,那么:

a^{\varphi(m)}\equiv1\pmod m

注意:这里的 m 不需要是质数。 ::::info[什么是 \varphi(m)] 欧拉函数 \varphi(m) 就是 1m 中与 m 互质的个数,特别的,\varphi(1)=1

例如:\varphi(6)=2,因为 (1,6)=1(5,6)=1。 :::: ::::info[闲话] 容易发现,如果 m 是质数,那么 \varphi(m)=m-1,所以我们说费马小定理是欧拉定理的特殊版本。 ::::

证明

不会证 qwq。

此处给一个感性的理解。

在费马小定理中,我们可以想象成有 p-1 个点,我们在 1a(以下默认要取模)之间连一条有向边,在 aa^2 之间连一条有向边,以此类推,那么乘以 a 的操作就是通过有向边走到下一个顶点,那么从 1 开始走,经过所有边(p-1 条边)后回到原点,说明定理成立。

在欧拉定理中,由于 (a,m)=1,不断乘以 a 后得到的数肯定也与 m 互质,因此我们可以想象成有 \varphi(m) 个点,按照同样的方法连边,从 1 开始走,经过所有边(\varphi(m) 条边)后回到原点,说明定理成立。

但是,上述理解其实是有问题的。因为两种定理所构造的图都只有一个环,但是环的大小不一定是 \varphi(m),也有可能是它的约数,这在以后学到“阶”的时候会有提及。

所以才说我不会证明了,大家勿喷 qwq。

应用

首先是求逆元,不再赘述。

还有一个应用就是简化乘方运算:对于 (a,m)=1,我们有

a^b\equiv a^{b \mod \varphi(m)}\pmod m

这样能够简化运算复杂度到 O(\log \varphi(m)),当然前提是你会快速幂。

补充:欧拉函数的求法

第一种方法:枚举,时间复杂度 O(m),不太行。

下面分析更快的做法。

m=30 为例,它有三个质因数 \{2,3,5\}

那么:

至少 包含一个质因数的数有 \dfrac{30}{2}+\dfrac{30}{3}+\dfrac{30}{5} 个。

至少 包含两个质因数的数有 \dfrac{30}{2\times 3}+\dfrac{30}{2\times 5}+\dfrac{30}{3\times 5} 个。

至少 包含三个质因数的数有 \dfrac{30}{2\times 3\times 5} 个。

根据容斥原理,有:

\begin{aligned} \varphi(30)&=30-(\dfrac{30}{2}+\dfrac{30}{3}+\dfrac{30}{5}-\dfrac{30}{2\times 3}-\dfrac{30}{2\times 5}-\dfrac{30}{3\times 5}+\dfrac{30}{2\times 3\times 5})\\ &=30-15-10-6+5+3+2-1\\ &=8 \end{aligned}

但同时,我们又有

\begin{aligned} \varphi(30)&=30-(\dfrac{30}{2}+\dfrac{30}{3}+\dfrac{30}{5}-\dfrac{30}{2\times 3}-\dfrac{30}{2\times 5}-\dfrac{30}{3\times 5}+\dfrac{30}{2\times 3\times 5})\\ &=30\times(1-\dfrac{1}{2}-\dfrac{1}{3}-\dfrac{1}{5}+\dfrac{1}{2\times 3}+\dfrac{1}{2\times 5}+\dfrac{1}{3\times 5}-\dfrac{1}{2\times 3\times 5})\\ &=30\times(1-\dfrac{1}{2})\times(1-\dfrac{1}{3})\times(1-\dfrac{1}{5})\\ &=8 \end{aligned}

自己算一下就清楚了。 ::::info[容斥原理] 集合都学过吧。

容斥原理就是说,如果你想要求 n 个集合的并集,你可以先把 n 个集合加起来,再减去任选两个集合的交集,再加上任选三个集合的交集,再……最后得到的答案就是 n 个集合的并集了。

相当于枚举每次取了几个集合进行交集运算(一个就是它本身),再枚举选取的集合,选了奇数个集合就加到答案里,偶数个就减掉。

具体的话自己看一下,此处不是重点。 :::: 推广到 m(证明看这里,但很难所以感性理解一下就好了),如果它有 k质因数,分别是 \{p_1,p_2,\cdots,p_k\},那么:

\varphi(m)=m\times(1-\dfrac{1}{p_1})\times(1-\dfrac{1}{p_2})\times\cdots\times(1-\dfrac{1}{p_k})

那么具体怎么找质因数呢?

我们可以从 2 开始枚举,每次枚举到一个就把 m 里面的这个因子除干净,保证接下来不会找到非质数因子。

由于大于 \sqrt{m} 的质因子只有一个,我们最后只要再判定一下 m 是不是大于 1,是的话就说明还有一个(就是现在的 m)。

代码如下。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

ll m;

int main() {
    scanf("%lld", &m);
    ll phi = m; // 欧拉函数
    for (ll i = 2; i <= sqrt(m); i++) {
        if (m % i == 0) {
            phi = phi - phi / i; // 避免浮点数精度误差
            while (m % i == 0) m /= i;
        }
    }
    if (m > 1) phi = phi - phi / m;
    printf("%lld\n", phi);
}