学习笔记:乘法逆元
乘法逆元
引入
在日常生活训练中常常有题目的答案很大,以至于超出了整形变量所能表示的范围。这种时候善良的出题人通常会选择让我们求出答案对 , 之类的数取模的值,这样可以避免麻烦大家写高精度。然而,这种方式也带来了一个问题:
首先观察一下这几个式子: 通常情况下我们为了防止溢出都会采取边计算边取模的方式。对于加、减、乘来说都是问题不大的,在除法上情况却有一点不同: 显然在上式中出现了 这种比较麻烦的情况,此时就不再适合直接取模了。
为了解决模意义下的除法问题,我们引入了逆元。 其实可以看做模 意义下的 ,那么在模 意义下, 就可以变形为 。
实际上在模 意义下 ,所以上面的式子可以这样计算:
定义
在数论中,如果 ,我们就说 和 在模 意义下互为乘法逆元,记作 。
求法
费马小定理
费马小定理是数论当中非常重要的定理(然而我早已忘得一干二净以至于被当众嘲笑),叙述如下:
若 是质数,且 ,则有 。
由乘法逆元定义推导可知 ,于是有: 也就是说,要求 在模 意义下的乘法逆元,只要求 即可。至于乘方的话可以用快速幂浅浅优化一下。
int qpow(int a, int b){
int res = 1;
while(b > 0){
if(b & 1){res *= a;res %= p;}
a *= a;a %= p;b >>= 1;
}
return res;
}
int inv(int n, int p){return qpow(n, p - 2);}
扩展欧几里德
原理和求解线性同余方程差不多。
int exgcd(int a, int b, int &x, int &y){
if(b == 0){
x = 1;y = 0;
return b;
}
int d = exgcd(b, a % b, y, x);
y -= a / b * x;
return d;
}
int inv(int n, int p){
int x, y;
exgcd(n, p, x, y);
return (x % p + p) % p;
}
线性递推
求出 中每个数关于 的逆元。
如果对于每个数进行单次求解,以上两种方法就显得慢了,很有可能超时,所以下面来讲一下如何线性()求逆元。
首先,很显然的 ;
对于 ,有 恒成立,故在 下 的逆元是 ,而这是推算出其他情况的基础。
其次对于递归情况 ,我们令 ,,有 。再放到 意义下就会得到:;
两边同时乘 :
再带入 ,有 ,有:
我们注意到 ,而在迭代中我们完全可以假设我们已经知道了所有的模 下的逆元 。
故我们就可以推出逆元,利用递归的形式,而使用迭代实现:
使用 来防止出现负数。
另外我们注意到我们没有对 inv[0]
进行定义却可能会使用它:当 成立时,我们在代码中会访问 inv[p % i]
,也就是 inv[0]
,这是因为当 时不存在 的逆元 。如果 与 不互素时不存在相应的逆元(当一般而言我们会使用一个大素数,比如 来确保它有着有效的逆元)。因此需要指出的是:如果没有相应的逆元的时候,inv[i]
的值是未定义的。
另外,根据线性求逆元方法的式子:
递归求解 , 直到 返回 。
中间优化可以加入一个记忆化来避免多次递归导致的重复,这样求 中所有数的逆元的时间复杂度仍是 。
注意:如果用以上给出的式子递归进行单个数的逆元求解,目前已知的时间复杂度的上界为 。算法竞赛中更好地求单个数的逆元的方法有扩展欧几里得法和快速幂法。
#include <iostream>
#define MAXN 3000005
using namespace std;
int n, p, inv[MAXN];
int main(){
cin >> n >> p;inv[1] = 1;
for(int i = 2 ; i <= n ; i ++)inv[i] = (p - p / i) * inv[p % i] % p;
for(int i = 1 ; i <= n ; i ++)cout << inv[i] << endl;
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效