逆元

对于逆元其实说难不难,说简单也不简单。
概念:
对于a * x≡b(mod m)这个方程如果我们要求解的话其实是比较复杂的,可是如果我们可以求出a * y≡1(mod m)中的y的话,在上面那个方程上同乘以y就可以得到,x=b * y,是不是很神奇,我们也称y是a在mod m的条件下的逆元,写作x ^ -1。
注意:
如果m是p的倍数,那么m在mod p的意义下是没有逆元的!!!
求法:
对于我们的逆元求法有四种,这四种各有千秋,我们要根据题目来决定采用哪种方法。
拓展欧几里得:
不知道拓展欧几里得的点这
对于求解这个方程: a * y≡1(mod m),我们其实可以这么写a * y+k * m=1,(这个k为任意整数),这个其实就是我们的拓展欧几里得,当然前提是gcd(a,m)=1
代码:

#include
#include
using namespace std;
int x,y;
void exgcd(int a,int b)
{
    int k;
    if(b==0)//边界 
    {
        x=1;
        y=0;
        return ;
    }
    exgcd(b,a%b);
    k=x;//因为x和y都会改变,所以我们开个变量储存
    x=y;//直接代入我们的公式 
    y=k-a/b*y;
}
int main()
{
    int a,b;
    scanf("%d %d",&a,&b);//我们要求a*y≡1(mod b) 
    exgcd(a,b);//要求a*x+b*y=gcd(a,b) 
    printf("%d",(x+b)%b);//因为求出来的可能是负数所以我们转化成正数 
    return 0;
} 

快速幂:
这个其实是用费马小定理来求的。
如果p是素数(也叫质数),对于任意的x ^ p≡x(mod p),这个定理就是费马小定理,当且仅当x无法被p整除可以得到,x ^ (p-1)≡1(mod p)(因为如果x可以被p整除的话x mod p一定为0,而不可能为1),然而我们再同除以一个x,可以得到x^-1≡x ^ (p-2)(mod p),所以我们只要求出x的p-2次方mod p就可以了,这就是快速幂。
代码:

#include
#include
using namespace std;
long long pow(long long a,long long b,long long mod)
{
	long long ans=1;
	while(b) 
	{
		if(b%2==1)//如果b是奇数 
			ans=(ans*a)%mod;//因为b最后一定会变为1,所以我们只要在这边计算ans就好了 
		a=(a*a)%mod;//a尝龟乘 
		b=b/2;
	}
	return ans;
}
int main()
{
	long long x,p,ans;
	scanf("%lld %lld",&x,&p);//求x在mod p意义下的方程
	ans=pow(x,p-2,p);
	printf("%lld",ans);
	return 0;
}

线性递推:
这个的时间复杂度是O(n)的,如果单单求一个逆元的话是比上面的都慢的,可如果要求多个的话这个就显现出来的优势。
我们线性递推其实就是找出逆元与逆元之间的关系,通过已知的逆元求出未知的逆元。
推导:
首先我们设p=k * i+r,然后可以知道k * i+r≡0(mod p)(因为k * i+r本身等于p,所以p mod p为0),然后我们两边同乘r^-1 * i ^ -1,就得到k * r ^ -1 +i ^ -1 ≡0(mod p),移项得到i ^ -1≡-k * r ^ -1(mod p),我们可以知道k其实就是p/i下取整,也就是⌊ p/i ⌋,r其实就是p%i,(这个好好想想是重点),所以我们得到公式:i ^ -1≡-⌊ p/i ⌋ * (p%i) ^ -1(mod p)
代码:

#include
#include
using namespace std;
long long inv[3000100];
int main()
{
	long long n,p;
	inv[1]=1;//边界,因为1的逆元一定是1 
	scanf("%lld %lld",&n,&p);//求1到n中所有在mod p的情况下的逆元 
	for(int i=2;i<=n;i++)
		inv[i]=(-(p/i)*inv[p%i]%p+p)%p;//根据公式递推,加p的原因是把负数转化成正数 
	for(int i=1;i<=n;i++)
		printf("%lld\n",inv[i]);//输出答案 
	return 0;
 } 

线性递推求阶乘逆元:
为什么要有这个,因为组合数也可以用阶乘逆元求,所以我们也介绍一下。
推导:
我们定义inv[i]表示i!的逆元,
我们可以知道inv[i+1]=(1/(i+1)!) ^ -1,(因为csdn比较难渲染数学公式,所以如果不方便看的话可以自己在纸上写),我们同乘i+1就变成了,inv[i+1]*(i+1)=(1/i!) ^ -1=inv[i],所以我们可以得到:inv[i+1] * (i+1)=inv[i],所以我们先求出n!的逆元,再倒推回来。n!的逆元怎么求?用费马小定理或者拓展欧几里得。
代码:

#include
#include
using namespace std;
long long inv[3000100]; 
long long pow(long long a,long long b,long long mod)
{
   long long ans=1;
   while(b) 
   {
   	if(b%2==1)//如果b是奇数 
   		ans=(ans*a)%mod;//因为b最后一定会变为1,所以我们只要在这边计算ans就好了 
   	a=(a*a)%mod;//a尝龟乘 
   	b=b/2;
   }
   return ans;
}
int main()
{
   long long n,p;//求1到n在mod p意义下所有阶乘的逆元
   inv[1]=inv[0]=1;
   scanf("%lld %lld",&n,&p);
   for(int i=1;i<=n;i++)//我们先求一遍阶乘 
   	inv[i]=(inv[i-1]*i)%p;
   inv[n]=pow(inv[n],p-2,p);//用费马小定理求逆元 
   for(int i=n-1;i>=1;i--)//倒推 
   	inv[i]=(inv[i+1]*(i+1))%p;
   for(int i=1;i<=n;i++)
   	printf("%lld\n",inv[i]); 
   return 0;
}

上面的是费马小定理的。

#include
#include
using namespace std;
long long inv[3000100],x,y; 
void gcd(long long a,long long b)
{
	if(b==0)//边界 
	{
		x=1;
		y=0;
		return ;
	}
	gcd(b,a%b);
	long long k=x;//因为x和y都会改变,所以我们开个变量储存 
	x=y;
	y=k-a/b*y;//直接代入我们的公式 
	return ;
}
int main()
{
	long long n,p;//求1到n在mod p意义下所有阶乘的逆元
	inv[1]=inv[0]=1;
	scanf("%lld %lld",&n,&p);
	for(int i=1;i<=n;i++)//我们先求一遍阶乘 
		inv[i]=(inv[i-1]*i)%p;
	gcd(inv[n],p);
	inv[n]=(x+p)%p;//用费拓展欧几里得求逆元 
	for(int i=n-1;i>=1;i--)//倒推 
		inv[i]=(inv[i+1]*(i+1))%p;
	for(int i=1;i<=n;i++)
		printf("%lld\n",inv[i]); 
	return 0;
}

上面是拓展欧几里得的。
以上就是逆元的所有知识了,希望大家看后可以有个理解。如果有不懂得欢迎留言。

你可能感兴趣的:(数论,算法)