逆元

對於逆元其實說難不難,說簡單也不簡單。
概念:
對於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<iostream>
#include<cstdio>
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<iostream>
#include<cstdio>
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<iostream>
#include<cstdio>
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<iostream>
#include<cstdio>
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<iostream>
#include<cstdio>
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;
}

上面是拓展歐幾里得的。
以上就是逆元的所有知識了,希望大家看後可以有個理解。如果有不懂得歡迎留言。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章