之前学习了一些逆元的知识点,在此做一个小总结。
定义
当我们在计算一些算式时会发现,当我们计算 %
时,可以转换为(a%m)
(b%m),但
%m就不能这样转换。这个时候该怎么办呢?我们知道
,所以我们就可以找到x使
(mod m),(当然,这里要求b和m互质,否则没有对应解,这里就不证明了)在这里x其实和
是等价的,这时x就是b在mod m意义下的逆元。下面讲解几种求法:
题目
给定正整数 n,p,求 [1,n] 中所有整数在模 p 意义下的乘法逆元。
a 模 p 的乘法逆元定义为 ax≡1(modp) 的解。
求法
下面两个方法适用于求解单个逆元(当然解这道题肯定是超时的啦)。
exgcd方法
一种方法是利用扩展GCD来求。这里先从GCD讲起。
GCD
GCD,就是求两个数的最大公约数。一个一个质数的试太慢了,复杂度应该在O()。我们来优化一下:
设这两个数分别为a和b, 为 x,a mod b 为 y。那么就能得到:
假设d为a,b两个数的公约数,那么等式两边同除以d,a除以d为整数,b除以d也为整数,所以等式右边为整数,那么等式左边也是整数,所以d也是y的约数。所以a,b的公约数同时也是b,a mod b的公约数。
反过来,假设d为b,a mod b两个数的公约数,依旧设 为 x,a mod b 为 y。那么就能得到:
等式两边同除以d,b除以d为整数,y除以d也为整数,所以等式右边为整数,那么等式左边也是整数,所以d也是a的约数。所以b,a mod b的公约数同时也是a,b的公约数。
综上,我们就能得出:gcd(a,b)=gcd(b,a mod b),当a mod b为0时,就说明能整除了,那么此时的b就是最大公约数。
代码
cpp
long long gcd(long long a,long long b){
if(b==0){
return a;
}
return gcd(b,a%b);
}
EXGCD
EXGCD,就是找到一对x,y使得 。下面是证明:
设,
要使式子恒等,则
当b=0时,我们一般将x设为1,y则为0,然后递归求解。
代码
cpp
#define ll long long
ll x,y;
ll exgcd(ll a,ll b){
if(!b){
x=1;
y=0;
return a;
}
ll d=exgcd(b,a%b);
ll x1=x,y1=y;
x=y1;
y=x1-a/b*y1;
return d;
}
如何求逆元?我们设ax+my=gcd(a,m)。因为a和m互质,所以gcd(a,m)=1,ax+my也等于1。等式两边同mod m,等式右边仍然为1,等式左边my mod m等于0,所以ax mod m等于1。如果要求的为正数解,那么就要调整一下。
exgcd方法代码
cpp
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll n,p,x,y;
ll exgcd(ll a,ll b){
if(!b){
x=1;
y=0;
return a;
}
ll d=exgcd(b,a%b);
ll x1=x,y1=y;
x=y1;
y=x1-a/b*y1;
return d;
}
int main(){
scanf("%lld%lld",&n,&p);
for(int i=1;i<=n;i++){
ll ans=exgcd(i,p);
printf("%lld\n",(x%p+p)%p);
}
return 0;
}
费马小定理+快速幂
这种方法适用于模数p是质数的情况。费马小定理如下:
设 𝑝
是素数.对于任意整数 𝑎
且 𝑝 ∤𝑎
,都成立 𝑎^𝑝−1 ≡1 (mod 𝑝)
.
由此,可知
所以就有一个简单的答案:。这个数怎么算?快速幂就行啦。
费马小定理+快速幂代码
cpp
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll n,p;
ll quick(ll a,ll k){
ll ans=1;
while(k){
if(k%2)ans=ans*a%p;
a=a*a%p;
k/=2;
}
return ans;
}
int main(){
scanf("%lld%lld",&n,&p);
for(int i=1;i<=n;i++){
ll anss=quick(i,p-2);
printf("%lld\n",anss);
}
return 0;
}
接下来两个方法适用于求解多个逆元。
正推
这是一种从1到n的方法。1的逆元是1.假设当前到了a,可得:
两边同模p,可得
两边同乘a的逆元和(p mod a)的逆元,可得(将a的逆元表示为)
移项,得
这样,就可以通过前面的逆元来求出当前数的逆元。
正推代码
cpp
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll n,p,ans[3000500];
int main(){
scanf("%lld%lld",&n,&p);
ans[1]=1;
printf("%lld\n",1);
for(int i=2;i<=n;i++){
ans[i]=(p-p/i)*ans[p%i]%p;
printf("%lld\n",ans[i]);
}
return 0;
}
倒推
这种方法因为也会计算阶乘的逆元,适用于排列组合。
首先,先将从1到n的阶乘去模p的结果算出来,然后通过前面exgcd或费马小定理+快速幂的方法求解出n的阶乘的逆元,然后从后往前遍历。把n阶乘的逆元看做,那么n阶乘的逆元就是
,所从就能求出n到1阶乘的逆元。最后,跟前面一样,从1到n计算i的逆元,就是
。
倒推代码
cpp
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll n,p,fact[3000500],inv[3000500];
ll quick(ll a,ll k){
ll ans=1;
while(k){
if(k%2)ans=ans*a%p;
a=a*a%p;
k/=2;
}
return ans;
}
int main(){
scanf("%lld%lld",&n,&p);
fact[0]=1;
for(int i=1;i<=n;i++){
fact[i]=fact[i-1]*i%p;
}
inv[n]=quick(fact[n],p-2);
for(int i=n-1;i>=1;i--){
inv[i]=inv[i+1]*(i+1)%p;
}
for(int i=1;i<=n;i++){
ll ans=inv[i]*fact[i-1]%p;
printf("%lld\n",ans);
}
return 0;
}
如果大家有其他想法的,可以补充。