蒟蒻初次写笔记,一定有许多不完善的地方,欢迎大佬们来踩一踩!
逆元的概念
什么是逆元?我们在提及逆元的时候通常指的是乘法逆元。满足
a*k≡1 (mod p)
的k值就是a关于p的乘法逆元。
逆元的作用
首先来举一个例子:
假设在一种算法中,现有a、b和p三个整数,我们希望求出 (a/b) mod p。
乍一看这样的计算很简单,按照正常顺序算即可。但是在很多程序里,a、b的值需要自行求解,这时要考虑在求解过程中会不会有变量溢出的现象发生。
那么怎么办呢?我们可以试着把取模运算提前,从而得到 (a mod p)/(b mod p)。
然而试了几个数,发现WA了。这是怎么了?
这是因为取模等式的变形中不涉及除法,且其推导式中也没有除法。关于取模运算的几个基本性质,可以访问这里。
但是根据取模等式内容,我们却可以推导出(a*k) mod p这样的方法,其中k是b关于p的逆元。
让我们来证明一下这个方法的可行性:(证明过程中涉及取模运算的变形,如果你不熟悉请先点击上面的链接了解)
逆元的正确性
首先,我们已经有了k 是b关于p的逆元的前提条件了。
那么我们可以得到b*k≡1 (mod p)
进而得到b*k=p*k+1
则k=(p*x+1)/b
在得到k之后我们可以将它代入之前的式子(a*k) mod p得到
(a*(p*x+1)/b) mod p
=((a*p*x)/b+a/b) mod p (乘法分配律)
=(((a*p*x)/b) mod p+(a/b) mod p) mod p
=((p*(a*b)/b) mod p+(a/b) mod p) mod p
因为 (p*(a/b)/b) mod p=0
所以 原式等于 (a/b) mod p。
实现求逆元
考虑使用扩展欧几里德(Extended Gcd)算法来实现这个求解方法。
为了使这一算法,首先介绍欧几里得算法
欧几里得算法是一种以较快速度求解两个数的最大公约数的方法。由于本方法比较简单,故直接给出代码。
int Gcd(int a,int b){
if(b==0) return a;
else gcd(b,a%b);
}
假设我们现在已经求得Gcd(a,b),那么对于a*x+b*y=Gcd(a,b),一定有至少一组x,y满足等式。
在a>b的前提下,我们分两种情况考虑:
在第一种情况下,b=0。那么此时Gcd(a,b)=a,则此时必有x=1,y=0。
在第二种情况下,a>b>0,那么此时应该有
a*x1+b*y1=Gcd(a,b) 我们称其为1式
b*x2+(a mod b)*y2=Gcd(b,a mod b) 我们称其为2式
根据之前的欧几里得算法,我们能得到Gcd(a,b)=Gcd(a,a mod b),因此1式和2式可以一起变为:
a*x1+b*y1=b*x2+(a mod b)*y2 我们称其为3式
为了理解下面的内容,我们有必要再次重温取余的计算方式。对于 a mod b,实际上我们是在计算 a-[a/b]*b。请注意此处 [ ] 的意义是取最大的整数使其小于方括号内的数。例如[3.254]=3。
有了这样的基础,我们很容易地将3式进行变形,最终得到:
a*x1+b*y1≡a*y2+b(x2-[a/b]y2)
根据恒等式定理,我们能推得
x1=y2 且 y1=x2- [a / b] *y2。
那么我们只要能获得x2,y2,那么我们就能获得x1,y1。
以上方法均可以通过递归实现,相应的代码如下:
/*
import guide:
int x,int y should be set as a public variable
ALWAYS call the function getAnswer()
DO NOT call the function _exGcd() for it is not complete.
*/
int _exGcd(int a,int b,int &x,int &y){
if(b==0){
x=1;
y=0;
return a;
}
int res=_exGcd(b,a%b,x,y);
int aux=x;
x=y;
y=aux-(a/b)*y;
return res;
}
int getAnswer(int a,int b){
int ans;
_exGcd(a,b,x,y);
ans=x+b;//bookmark 1#
return ans;
}
至此,得到了a关于b的逆元。如果需要进行取模,应把注释为“bookmark 1#”的代码行替换成
ans=(x+b)%MOD;// MOD is the target nuber for modulo