乘法逆元
[用途]
求解关于a/b(mod p)的问题
[介绍]
我们假设x为a的乘法逆元,为什么要求乘法逆元呢?当a/b非常大导致溢出时,程序必然出错。我们可以将a/b(mod p)转化为ax(mod)p;根据乘法逆元的定义,在模p的意义下有:ax≡1 (mod) p
如果乘法逆元x存在,a,p一定互素!
[适用条件]
- 当p为素数时,可以使用费马小定理求解
- 当p为合数时,使用欧拉定理求解
- 不管p是素数还是合数,都可以使用拓展欧几里得求解(推荐)
拓展欧几里得
对于ax≡1 (mod) p可以化为ax+bp=1;如果乘法逆元x存在,a,p一定互素,即gcd(a,p)=1;反之a,p不互素,则乘法逆元x不存在;而拓展欧几里得刚好用于求解ax+by=gcd(a,b),我们可以根据gcd(a,b)是否等于1来判断乘法逆元x是否存在。
#include<iostream>
using namespace std;
typedef long long LL;
LL exgcd(LL a,LL b,LL &x,LL &y)
{
if(!b){
x=1;
y=0;
return a;
}
LL gcd=exgcd(b,a%b,y,x);
y-=a/b*x;
return gcd;
}
int main()
{
LL a,b;
while(cin>>a>>b){
LL x,y;
if(exgcd(a,b,x,y)!=1)cout<<"a的乘法逆元不存在"<<endl;
else cout<<"a的乘法逆元为: "<<x<<endl;
}
return 0;
}
费马小定理
当a是正整数,p是素数时,有a^(p-1)≡1 (mod) p,则乘法逆元x=a^(p-2),根据快速幂求解即可
#include<iostream>
using namespace std;
typedef long long LL;
LL ksm(LL x,LL n,LL mod)
{
x%=mod;
LL ans=1;
while(n){
if(n&1)ans=ans*x%mod;
x=x*x%mod;
n>>=1;
}
return ans;
}
int main()
{
LL a,mod;
while(cin>>a>>mod)cout<<"a的乘法逆元为:"<<ksm(a,mod-2,mod)<<endl;
return 0;
}
欧拉定理
有a^phi(p)≡1 (mod) p,a的乘法逆元x即为phi(p)
积性函数:函数f(x)对任意互素的两个数x1,x2,有f(x1*x2)=f(x1)*f(x2)
完全积性函数:函数f(x)对任意两个正整数数x1,x2,有f(x1*x2)=f(x1)*f(x2)
对于积性函数有:f(p)=f(p1^k1)*f(p2^k2)*f(p3^k3)....*f(pn^kn),其中p1、p2...pn为不同的素数,k1、k2....kn为非负整数
欧拉函数是积性函数,也有phi(p)=phi(p1^k1)*phi(p2^k2)*phi(p3^k3)....*phi(pn^kn)
对于欧拉函数还有以下结论成立:
结论一:p为素数:phi(p)=p-1;
结论二:p为素数且k是p的倍数:phi(p*k)=phi(k)*p;
结论三:p,q互素:phi(p*q)=phi(p)*phi(q)
结论四:p为素数,k为正整数,phi(p^k)=p^k-p^(k-1)=(p-1)*p^(k-1),
欧拉筛法 时间复杂度为O(n)
#include<iostream>
#include<cstring>
#include<cmath>
using namespace std;
typedef long long LL;
const LL N=1e6+99;
LL su[N],phi[N],num;
bool vis[N];
void init()
{
memset(vis,true,sizeof(vis));
num=0;
vis[0]=vis[1]=false;
phi[1]=1; /*phi[1]特例*/
for(LL i=2;i<=N;++i){ /*欧拉筛法*/
if(vis[i]){
su[++num]=i;
phi[i]=i-1; /*结论一*/
}
for(LL j=1;j<=num&&i*su[j]<=N;++j){
vis[i*su[j]]=false;
if(i%su[j]==0){ /*结论二*/
phi[i*su[j]]=phi[i]*su[j];
break;
}
else phi[i*su[j]]=phi[i]*phi[su[j]];/*结论三*/
}
}
/*for(LL i=1;i<=100;++i)cout<<su[i]<<" ";
cout<<endl;
for(LL i=1;i<=100;++i)cout<<phi[i]<<" ";*/
}
int main()
{
init();
return 0;
}
一般的,p不会超过int范围,求phi(p)时如果选择欧拉筛法打表,时间复杂度为O(n),很有可能是超时的,所以我们可以选择时间复杂度为O(√n) (仅仅求单个) 的方法。
另一条重要的定理是算数基本定理:任何一个大于1的自然数 N,可以唯一分解成有限个质数的乘积,即N=(p1^k1)*(p2^k2)*(p3^k3)....(pn^kn),其中p1、p2...pn为不同的素数,k1、k2....kn为非负整数,而对于欧拉函数有
phi(p)=phi(p1^k1)*phi(p2^k2)*phi(p3^k3)....*phi(pn^kn),利用结论四:p为素数,k为正整数,phi(p^k)=p^k-p^(k-1)=(p-1)*p^(k-1);再结合筛法(埃式与欧拉都行)求2~√p的素数即可,时间复杂度O(√n)
#include<iostream>
#include<cstring>
using namespace std;
typedef long long LL;
const LL N=1e3+99;
LL su[N],num;
bool vis[N];
void init()
{
memset(vis,true,sizeof(vis));
num=0;
vis[0]=vis[1]=false;
for(LL i=2;i<=N;++i){
if(vis[i])su[++num]=i;
for(LL j=1;j<=num&&i*su[j]<=N;++j){
vis[i*su[j]]=false;
if(i%su[j]==0)break;
}
}
/*for(LL i=1;i<=100;++i)cout<<su[i]<<" ";
cout<<endl;*/
}
LL get(LL p)
{
LL ans=1;
for(int i=1;i<=num;++i){
if(p%su[i]==0){
int j=-1;
while(p%su[i]==0){
++j; /*求指数j即结论四中的k-1*/
p/=su[i];
}
while(j--)ans*=su[i]; /*求p^(k-1)*/
ans*=(su[i]-1); /*求(p-1)*p^(k-1)*/
if(p==1)break;
}
}
return ans;
}
int main()
{
init();
LL p;
while(cin>>p)cout<<"phi(p):"<<get(p)<<endl;
return 0;
}