逆元:
建议先学的知识:
逆元介绍及应用(inv):
求解公式(a/b)%m 时,因b可能会过大,会出现爆精度的情况,所以需变除法为乘法:
设c是b的逆元,则有b*c≡1(mod m);
则(a/b)%m = (a/b)*1%m = (a/b)*b*c%m = a*c(mod m);
即a/b的模等于a*b的逆元的模;
做题时可能用到的公式:
b*c≡1(mod m)
b≡c^(-1) (mod m)//逆元的逆元==数本身
求单点逆元方法
第一种:扩展欧几里得算法
时间复杂度:O(logn)
从b*c≡1(mod m)可以得出关系式 :k*m+1 = b*c
->b*c+(-k)*m = 1
完全符合扩展欧几里得算法
代码如下:
#include<iostream>
#include<cstdio>
#define D long long
using namespace std;
D exgcd(D a,D b,D &x,D &y){
if(b==0){
x=1;y=0;return a;
}
D d=exgcd(b,a%b,y,x);
y-=a/b*x;
return d;
}
D inv(D b,D m){//b*c≡1(mod m) -> b*c+m*k = 1
D c,k;
D d=exgcd(b,m,c,k);//因为b,m互质,所以d==1 ,r就是m,c/=d可省略
c/=d;
D r=m/d;//r为多个解c1,c2,c3的差
c=(c%r+r)%r;
return c;
}
int main()
{
D b,m;
while(cin>>b>>m){
cout<<inv(b,m)<<endl;
}
}
第二种:费马小定理求解
时间复杂度:O(logn)
因为x^(p-1) ≡ 1(mod p),所以x * x^(p-2) ≡ 1 ( mod p ) ,即x的逆元为x^(p-2) 。注意:根据费马小定理,x与p互质时才可以用此方法。
代码如下:
#include<stdio.h>
#include<iostream>
#define D long long int
#define mod ((int)(1e9)+7)
using namespace std;
D swift(D a,D b)
{
D base=a,mul=1;
while(b)
{
if(b&1){mul=mul*base%mod;}
base=base*base%mod;b>>=1;
}
return mul;
}
D inv(D b,D m){
return swift(b,m-2);
}
int main()
{
D b,m;
while(scanf("%lld%lld",&b,&m)!=EOF)
{
printf("%lld\n",inv(b,m));
}
return 0;
}
第三种:人眼求解
时间复杂度:O (10 / 脑细胞强度)
在当N是质数,a是(N+1)的约数时,inv [a] = ( N + 1 )/a
比如 mod 37 时,19的逆元为2
第四种:通用方法直接解题
在题目求a/b%mod时,一种方法是改成a*inv[b]%mod,即求除数的逆元;另一种办法是改成a%(mod*b)/b。此方法为通用方法,并不像费马小定理和扩展欧几里得算法求逆元一样需要a和mod互质。
例如:17/3%7=5 -> 17%(3*7)/3=5
证明:
线性递推求解逆元
即对inv[1]初始化为1,从1到n可直接由公式求得
递推公式为:inv[i] = ( m - m / i ) * inv[ m % i ] % m
分析:
假设 t = m / i , k = m % i ;
-> t * i + k ≡ 0 ( mod m )
-> - t * i ≡ k ( mod m )
-> - t * 1 / k ≡ 1 / i ( mod m )
因为 1 / i * i ≡ 1 ( mod m ),所以可以用inv[ i ] 代替 1 / i
-> - t * inv [ k ] ≡ inv [ i ] ( mod m )
再替换 t,k
-> inv [ i ] = ( m - m / i ) * inv [ m % i ] % m
代码如下:
#include<iostream>
#include<cstdio>
#include<cstring>
#define D long long
using namespace std;
#define N 100100
#define mmm(a,b) memset(a,b,sizeof(a))
D inv[N];
D m;
void INinv(D m,D inv[],D n){
mmm(inv,0);
inv[1] = 1;
for(int i = 2; i <= n ; i++)
inv[i] = ( m - m / i ) * inv[ m % i ] % m;
}
void OUTinv(D m,D inv[],D n){
cout<<"when mod is "<<m<<endl;
for(int i=1;i<=n;i++){
cout<<inv[i]<<' ';
}
cout<<endl;
}
int main()
{
D n=20;
while(cin>>m){//m is mod which is prime
INinv(m,inv,n);
OUTinv(m,inv,n);
}
}
逆元在阶乘运算上的应用
例如求组合数C(a,b) % mod
按照公式 C(a,b) = a! / b! / ( a - b )!
此时便可以运用逆元防止精度的问题,因为是%mod,所以乘法基本不会出错。
-> C(a,b) % mod = a! * inv [b!] * inv [( a - b )!] % mod
接下来就是求inv[i!]的事情了。
方法一:
i!= 1 * 2 * …. * i
∴ 理所当然只要拿1到 i 所有的数的逆元相乘便是 i!的逆元了
∴ inv [ i! ] = inv [1] * inv [2] * … * inv [i]
证明:inv [ i! ] * i! % mod = 1 = ( inv [1] * 1 % mod ) * ( inv [2] * 2 % mod ) * … * ( inv [i] * i % mod )
代码:
inv[1]=inv_fac[i]=1;
for(int i=2;i<N;i++){
inv[i]=(M-M/i)*inv[M%i]%M; //递推求inv[i]
inv_fac[i]=inv[i]*inv_fac[i-1]%M; //inv_fac[i] 代表 inv[i!]
}
方法二:
这个方法,本来想直接按照网上的公式写上去算了的,但是为了那些和我一样好学的人,也为了让自己的博客看起来不会不负责任,我看了好久,终于想明白了其中的道理。(可能是我笨才没有秒懂吧。。。)
这个方法首先需要根据费马小定理,得出inv_fac [n] = n! ^ (mod-2) ,然后前面的项即可用后面的项按公式推出。
公式还是挺6的,我自己默默推了一年。。。
-> inv [n!] = inv [( n - 1 )! * n]
看了第一种方法应该知道 inv [a * b] = inv [a] * inv [b]
-> inv [n!] = inv [(n - 1)!] * inv [n]
两边同时乘以 inv [inv [n]]即inv [n]的逆元,左边,因为逆元的逆元即是自身,即n,所以变为 inv [n!] * n;右边,在mod的情况下,inv [n] 乘以inv [n]的逆元为1,变为inv [(n - 1)!]
-> inv [(n - 1)!] = inv [n!] * n
-> inv_fac[i]=(inv_fac[i+1]*(i+1))%mod;
附上代码:
inv_fac[n]=swift(fac[n],mod-2); //快速幂
for(int i=n-1;i>=1;i--)
inv_fac[i]=(inv_fac[i+1]*(i+1))%mod;
例题
题解:逆元在阶乘上的运用