概念引入
学习逆元之前,首先要知道什么叫逆元
- 逆元这个概念通常是用来解决除法求模问题的,关于求模,有下面的公式
( a + b ) % c = a % c + b % c ( a − b ) % c = a % c − b % c ( a ∗ b ) % c = ( a % c ∗ b % c ) % c (a+b)\%c=a\%c+b\%c\\(a-b)\%c=a\%c-b\%c\\(a*b)\%c=(a\%c*b\%c)\%c (a+b)%c=a%c+b%c(a−b)%c=a%c−b%c(a∗b)%c=(a%c∗b%c)%c - 可以看出,对于除法没有相应的取模公式,这是因为除法的类似公式是不成立的,可以举例说明。有时候a和b很大,计算机存不下,不能计算之后再取模,所以我们想找到一些途径在中间过程中取模,这样,就想能不能变除法为乘法呢?
- 设b*k%c=1,则有
( a b ) % c = ( a b % c ∗ ( b k ) % c ) = ( a ∗ k ) % c (\frac{a}{b})\%c=(\frac{a}{b}\%c*(bk)\%c)=(a*k)\%c (ba)%c=(ba%c∗(bk)%c)=(a∗k)%c
这样就把除法转化为了乘法,这里面的k就叫做b关于c的乘法逆元,写成数学表达式就是
b k ≡ 1 ( m o d c ) bk\equiv1\pmod c bk≡1(modc)
读作bx和1对于模c同余 - 初学时很容易就能联想到我们曾经学过的倒数这一概念,也就是一个数的倒数乘以自身等于1,在这里逆元是数论倒数(算术倒数)并非普通意义上的倒数,是取模引入的概念,不要弄混
求解方法
费马小定理
这个定理的内容是这样的:
如果p为质数,a不是p的倍数,那么有
a p − 1 ≡ 1 ( m o d p ) a^{p-1}\equiv1\pmod p ap−1≡1(modp)
- 关于这个定理,不作证明,可以自行百度。研究问题有时候也不要太面面俱到,关系不大。以有涯逐无涯,殆矣
- 观察此定理和上面的式子,可以发现这个式子可以变化为
a × a p − 2 ≡ 1 ( m o d p ) a\times a^{p-2}\equiv1\pmod p a×ap−2≡1(modp)
正好和上面式子里的b和k对应,也就是说,a的乘法逆元为ap-2,当然,需要满足费马小定理的条件
模板题
尝试一下
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <vector>
#include <cmath>
#include <queue>
#include <stack>
#include <map>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int MAXN = 1e6 + 100;
const double eps = 1e-6;
int Data[MAXN];
ll GCD(ll a, ll b){
return b == 0 ? a : GCD(b, a % b);
}
ll fastpow(ll base, ll power, ll MOD){
ll ans= 1;
while(power){
if(power & 1) ans = ans * base % MOD;
base = base * base % MOD;
power >>= 1;
}
return ans % MOD;
}
int main(){
int t;
ll y, p;
cin >> t;
while(t--){
cin >> y >> p;
if(GCD(y, p) != 1) cout << -1 << "\n";
else cout << fastpow(y, p - 2, p) << "\n";
}
return 0;
}
- 使用时候注意需要满足条件
扩展欧几里德
- 还是这样一个式子
b k ≡ 1 ( m o d c ) bk\equiv1\pmod c bk≡1(modc)
- 回顾一下,b关于c的逆元为k,现在我们要求k,这里面b、c都已知
- 很容易能够知道GCD(b, c)或者说b和c的最大公约数应该为1,或者说b,c互质,如果不互质那么模就不会是1,所以这个方程等价于求下面这个方程的解
b x + c y = 1 bx+cy=1 bx+cy=1 - 求出的x即为逆元(还需要考虑负数的情况),至于方程的解法,因为b和c互质,右边是1,所以是一个明显的扩展欧几里德,如果让判断是否无解,那么b和c如果不互质,那么无解
- 如果解出来x是负数,那么需要通过c来调整x为正数,具体如下
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <vector>
#include <cmath>
#include <queue>
#include <stack>
#include <map>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int MAXN = 1e6 + 100;
const double eps = 1e-6;
int Data[MAXN];
void extend_gcd(ll a, ll b, ll &d, ll &x, ll &y){
if(b == 0){
x = 1;
y = 0;
d = a;
return;
}
extend_gcd(b, a % b, d, x, y);
ll tmp = x;
x = y;
y = tmp - a / b * y;
}
ll mod_reverse(ll y, ll p){
ll x, d;
extend_gcd(y, p, d, x, y);
return d == 1 ? (p + x % p) % p : -1;
}
int main(){
int t;
ll y, p;
cin >> t;
while(t--){
cin >> y >> p;
cout << mod_reverse(y, p) << "\n";
}
return 0;
}
模板题
- 可以用上面的两种算法尝试一下这道题,扩展欧几里得更快一些,但是最后一个点都是TLE,由于这道题比较特殊,它要求求解前n个数的逆元,而n范围又比较大,上面两种算法适用于的情况是单一计算逆元,或者计算数量较少的逆元,如果要求很多个数的逆元,需要使用更好的方法