乘法逆元学习笔记(初学但易理解)

基本概念

所谓乘法逆元,就是两个整数a和x相乘再用一个(非1正整数)数p对它们取模,若取模后所得的值等于1,那么x和a在模p条件下互为乘法逆元.
用同余方程表达即: a ∗ x ≡ 1 ( m o d   p ) {a*x≡1(mod~p)} ax1(mod p),
用一般方程表达为: a ∗ x − k ∗ p = 1 , ( k ∈ z ) {a*x-k*p=1,(k∈z)} axkp=1,(kz).
( a 存 在 逆 元 时 有 一 充 要 条 件 : g c d ( a , p ) = 1 即 a , p 互 质 ) {(a存在逆元时有一充要条件:gcd(a,p)=1即a,p互质)} (a:gcd(a,p)=1a,p).
这样是不是清楚多了?如果不是,左转百度百科.
(注:以下算法时间复杂度均为求一个数逆元的时间级)

逆元求法

1.费马小定理+快速幂

时间复杂度: O ( l o g n ) {O(logn)} O(logn)(总 O ( n ∗ l o g n ) {O(n*logn)} O(nlogn))
条件限制: p {p} p为质数.
(注:若p为合数,也可以用快速幂实现,不过要用上欧拉定理(小费马的一般形式),但用它来求逆元的实用性不大(要用欧拉筛 O ( n ) {O(n)} O(n).)
*(欧拉定理:若a、p互素,则有:
( φ ( p ) 为 [ 1 , p − 1 ] 的 整 数 中 与 p 互 质 的 数 的 个 数 {φ(p)为[1,p-1]的整数中与p互质的数的个数} φ(p)[1,p1]p) a φ ( p ) ≡ 1 ( m o d   p ) , 代 入 得 a φ ( p ) = x ∗ a {a^{φ(p)}≡1(mod~p),代入得a^{φ(p)}=x*a} aφ(p)1(mod p),aφ(p)=xa,
a φ ( p ) − 1 {a^{φ(p)−1}} aφ(p)1就是 a {a} a m o d   p {mod~p} mod p意义下的逆元 x {x} x)

由费马小定理得:
对 于 整 数 a 和 质 数 p , 若 p ∣ a , 那 么 a p ≡ a ( m o d   p ) ; {对于整数a和质数p,若p|a,那么a^p≡a(mod~p);} ap,pa,apa(mod p);
否 则 : a p − 1 ≡ 1 ( m o d   p ) {否则:a^{p-1}≡1(mod~p)} :ap11(mod p).
所以将该定理变形得 a p − 1 − k ∗ p = 1 ( k ∈ z ) {a^{p-1}-k*p=1(k∈z)} ap1kp=1(kz)
带入 a ∗ x − k ∗ p = 1 , ( k ∈ z ) {a*x-k*p=1,(k∈z)} axkp=1,(kz).得 x = a p − 2 {x=a^{p-2}} x=ap2.
然后由于题中自带的模数都比较大,所以你懂的 快速幂码起!

例题:【模板】乘法逆元

#include
using namespace std;

long long n,p;

inline long long ksm(int i,int cf)//开long long,有保障,但相对慢一点点.
{
	long long sum=1,mi=i;
	while(cf)
	{
		if(cf&1)sum=((sum%p)*(mi%p))%p;//模p不勤快,爆零两行泪.
		mi=((mi%p)*(mi%p))%p;
		cf=cf>>1;
	}
	return sum%p;
}

int main()
{
	scanf("%d%d",&n,&p);
	cout<<1<<endl;
	for(int i=2;i<=n;i++)
	cout<<ksm(i,p-2)<<endl;//输出答案
	return 0;
}

但。。。T了?WTF O ( n l o g n ) {O(nlogn)} O(nlogn)算法会T?尽管在其他板块这是个优秀的复杂度,但在数论方面太菜了 显然还能更优.
下面介绍另一种方法.

2.扩展欧几里得

时间复杂度: O ( l n   n ) {O(ln~n)} O(ln n)(总 O ( n ∗ l n   n ) {O(n*ln~n)} O(nln n))
条件限制:似乎整数就行.

由这个方程: a ∗ x − k ∗ p = 1 , ( k ∈ z ) {a*x-k*p=1,(k∈z)} axkp=1,(kz),令 k = − y , ( y ∈ z ) {k=-y,(y∈z)} k=y,(yz)
然后用求二元一次方程的方法,用扩展欧几里得算法求得:
a ∗ x + p ∗ y = 1 , ( k ∈ z ) {a*x+p*y=1,(k∈z)} ax+py=1,(kz)一组x,y的整数解 x 0 , y 0 {x0,y0} x0,y0 g c d ( x 0 , y 0 ) {gcd(x0,y0)} gcd(x0,y0),并检查gcd(x0,y0)是否为1,若不为1则不存在逆元,若为1,将 x 0 {x0} x0调整至 [ 0 , m − 1 ] {[0,m-1]} [0,m1]即可得到符合条件的解.

证明:
假如 p = 0 {p=0} p=0,由于 g c d ( a , p ) = 1 {gcd(a,p)=1} gcd(a,p)=1,因此 a = x = 1 {a=x=1} a=x=1.
假如 p ≠ 0 {p≠0} p=0,不妨假设 a = k ∗ p + r ( k 是 a 除 以 p 的 商 , r 是 余 数 ) {a=k*p+r( k是a除以p的商,r是余数)} a=kp+r(kap,r),并且我们已经求出了 p ∗ x + r ∗ y = 1 {p*x+r*y=1} px+ry=1的一组解(x0,y0).
p ∗ x 0 + ( a − k ∗ p ) ∗ y 0 = 1 {p*x0+(a-k*p)*y0=1} px0+(akp)y0=1
a ∗ x 1 + p ∗ y 1 = 1 {a*x1+p*y1=1} ax1+py1=1
p ∗ x 0 + a ∗ y 0 − k ∗ p ∗ y 0 = p ∗ ( x 0 − k ∗ y 0 ) + a ∗ y 0 = a ∗ x 1 + p ∗ y 1 {p*x0+a*y0-k*p*y0=p*(x0-k*y0)+a*y0=a*x1+p*y1} px0+ay0kpy0=p(x0ky0)+ay0=ax1+py1
x 1 = y 0 ; y 1 = x 0 − k ∗ y 0 = x 0 − ( a / p ) ∗ y 0 ; {x1=y0;y1=x0-k*y0=x0-(a/p)*y0;} x1=y0y1=x0ky0=x0(a/p)y0;
那么(x1,y1)就是 a ∗ x + p ∗ y = 1 {a*x+p*y=1} ax+py=1的一组解.

虽然上一题还是不能过,但它可以过这个题:(第一种方法只有20分)
【NOIPS2012】同余方程

#include
#define ll long long
using namespace std;

ll n,p,x,y;//开long long 保险.

inline ll exgcd(ll a,ll b,ll &x,ll &y)//扩欧
{
	if(b==0)
	{
		x=1,y=0;
		return a;
	}
	int r=exgcd(b,a%b,x,y);
	int t=x;
	x=y;
	y=t-(a/b)*y;
	return r;
}

int main()
{
	scanf("%lld%lld",&n,&p);
	exgcd(n,p,x,y);
	cout<<(x%p+p)%p<<endl;//可以处理负数
	return 0;
}

总结一下这个算法:速度较快,范围广,但证明的思维难度较大,相比之下这种更适合数论较好的人使用.
下面介绍能A掉上面板子题的方法.
另一个板子题:【模板】有理数取余,不过要注意这道题的快读可以用来代替高精度的运算,如下:(快读的新功能)

inline int read()//幸好没有负数qwq
{
	int i=0;char ch;
	while(!isdigit(ch)){ch=getchar();}
	while(isdigit(ch))
	{
		i=(i<<3)+(i<<1)+(ch-'0');
		i=i%p;//边模边算不会影响结果(取模运算法则在四则运算中只对除法不成立)
		ch=getchar();
	}
	return i;
}

更多有关扩欧的例题:青蛙的约会.

3.线性算法(类似于递推)

时间复杂度: O ( 1 ) {O(1)} O(1)(总 O ( n ) {O(n)} O(n))(能为所要求逆元的这个数的大小)
条件限制:只能从1开始算.

这个算法相对exgcd的优劣性非常明显,例如在求一段不是很大的一段很长的连续整数的逆元,它显然更优,但在求单个数或数个很大的数时exgcd就明显占上风了,所以在选算法时因题而异.

以下为某神牛的证明:
首先我们有一个 1 − 1 ≡ 1 ( m o d p ) {1^{-1}\equiv 1 \pmod p} 111(modp)
然后设 p = k ∗ i + r , ( 1 < r < i < p ) {p=k*i+r,(1p=ki+r,(1<r<i<p)
( k 是 p / i 的 商 , r 是 余 数 {k是p/i的商,r是余数} kp/i,r)。

再将这个式子放到 ( m o d p ) {\pmod p} (modp)意义下就会得到:
k ∗ i + r ≡ 0 ( m o d p ) ① {k*i+r \equiv 0 \pmod p ①} ki+r0(modp)
然后乘上 i − 1 , r − 1 {i^{-1} ,r^{-1}} i1,r1就可以得到:
k ∗ r − 1 + i − 1 ≡ 0 ( m o d p ) {k*r^{-1}+i^{-1}\equiv 0 \pmod p} kr1+i10(modp)
i − 1 ≡ − k ∗ r − 1 ( m o d p ) {i^{-1}\equiv -k*r^{-1} \pmod p} i1kr1(modp)
i − 1 ≡ − ⌊ p i ⌋ ∗ ( p   m o d   i ) − 1 ( m o d p ) ② {i^{-1}\equiv -\lfloor \frac{p}{i} \rfloor*(p \bmod i)^{-1} \pmod p②} i1ip(pmodi)1(modp)
由 于 ( p    m o d    i ) < i , 所 以 , 在 求 出 i − 1 之 前 , 我 们 早 已 求 出 ( p    m o d    i ) − 1 {由于 (p\; mod\; i) < i,所以,在求出 i^{-1}之前,我们早已求出 (p\; mod \;i)^{-1}} (pmodi)<i,,i1,(pmodi)1
因此用数组 n y [ i ] {ny[i]} ny[i]记录 i − 1 {i^{-1}} i1(i的逆元)
n y [ i ] = − p i   ∗ n y [ p    m o d    i ]    m o d    p {ny[i]=-\frac{p}{i}\ * ny[p\;mod\;i]\;mod\;p} ny[i]=ip ny[pmodi]modp;
不要以为到这里就结束了因为我们需要保证 i − 1 > 0 {i^{-1}>0} i1>0
所以,我们在②式右边    + p ( p    m o d    p = 0 ) , {\;+p( p\;mod\; p=0), } +p(pmodp=0),答案不变,
n y [ i ] = p − p i   ∗ n y [ p    m o d    i ]      m o d    p ; {ny[i]=p-\frac{p}{i}\ * ny[p\;mod\;i]\;\;mod\;p;} ny[i]=pip ny[pmodi]modp;
当然 n y [ 1 ] = 1 , n y [ 0 ] = 0 {ny[1]=1,ny[0]=0} ny[1]=1,ny[0]=0;
注意 f o r {for} for循环必须从2开始,不然会替换掉 n y [ 1 ] {ny[1]} ny[1]的值.
于是,我们就可以从前面推出当前的逆元了。
以下为第一题代码:

#include
#define N 3000005
#define ll long long
using namespace std;

ll n,p;
ll ny[N];

int main()
{
	scanf("%lld%lld",&n,&p);
	ny[1]=1;
	for(int i=2;i<=n;i++)
	ny[i]=(p-p/i)*ny[p%i]%p;//线性递推
	for(int i=1;i<=n;i++)
	printf("%d\n",ny[i]);
//	cout<<(ny[n]%b+b)%b<
	return 0;
}

主要运用

在带有除法的取余运算中,将除法化为乘法以避免某种情况下:
爆longlong或失精度(原因:(a/b)%p!=(a%p)/(b%p)很容易找反例证明).

如下面一道例题:
T100938 滞空

思想方法

这是一个物理&&逆元题.(假设从坐标 ( x 1 , y 1 ) {(x1,y1)} (x1,y1)跳到 ( x 2 , y 2 ) {(x2,y2)} (x2,y2))
针对向下的跳的情况我们由高中物理得: E = m ∗ g ∗ ( x 2 − x 1 ) 2 / ( 4 ∗ ∣ y 2 − y 1 ∣ ) {E=m*g*(x2-x1)^2/(4*|y2-y1|)} E=mg(x2x1)2/(4y2y1)
针对向上的情况我们有:
E = m ∗ g ∗ [ ( y 2 − y 1 ) + ( x 2 − x 1 ) 2 / ( 4 ∗ ∣ y 2 − y 1 ∣ ) ] {E=m*g*[(y2-y1)+(x2-x1)^2/(4*|y2-y1|)]} E=mg[(y2y1)+(x2x1)2/(4y2y1)].
(前两个应该高中及以上的都会推吧(小初的巨神表示不屑于此 ))
针对高度差为零的情况,我们只需要求一个斜抛运动的最小初速度就行了(也很好证的,所以我就不证了qwq )
最 后 得 出 : v m i n = g ( x 2 − x 1 ) , E = m g ( x 2 − x 1 ) / 2. {最后得出:v_{min}=\sqrt{g(x2-x1)},E=mg(x2-x1)/2.} :vmin=g(x2x1) ,E=mg(x2x1)/2.
最后用扩欧或小费马快速幂求逆元解决除法即可.

#include
#define in read()
#define N 1000005
#define int long long
using namespace std;

int n,m,g,pow1=0,pow2=0,ju,p=998244353,x,y;
struct zb{
int x,y;}a[N];

inline int in{
	int i=0;char ch;
	while(!isdigit(ch)){ch=getchar();}
	while(isdigit(ch)){i=(i<<3)+(i<<1)+(ch^48);ch=getchar();}
	return i;
}//快读加速

inline int exgcd(int a,int b,int &x,int &y)//扩欧求逆元(p是质数,也可用快速幂)
{
	if(b==0)
	{
		x=1,y=0;
		return a;
	}
	int r=exgcd(b,a%b,x,y);
	int t=x;
	x=y;
	y=t-(a/b)*y;
	return r;
}

inline int exgcdd(int a,int b,int &x,int &y)
{
	exgcd(a,b,x,y);
	return (x%p+p)%p;//这才是逆元,而x只是一个可行的二元方程解.
}

signed main()
{
//	freopen("jump.in","r",stdin);
//	freopen("jump.out","w",stdout);
	n=in,m=in,g=in;
	for(int i=1;i<=n;i++)
	a[i].x=in,a[i].y=in;
	int wei=m*g%p;
	for(int i=1;i<=n-1;i++)
	{
		int dtx=(a[i+1].x-a[i].x)%p,dty=(a[i+1].y-a[i].y)%p;
		int eg=exgcdd(abs(dty),p,x,y)%p,es=exgcdd(4,p,x,y)%p;
		if(dty)
		{	
			if(dty>0)pow2=(pow2+(wei*(dty)%p))%p;//判由低到高
			pow1=(pow1+(((wei%p)*(dtx*dtx%p)%p)*(eg*es%p)%p)%p)%p;//一定尽可能多的取模!
		}
		else//高度一样的情况
		{
			int er=exgcdd(2,p,x,y)%p;
			pow1=(pow1+(wei*(dtx*er%p)))%p;
		}
	}
	int poww=(pow1+pow2)%p;
	//printf("%lld %lld\n",pow1,pow2);
	printf("%lldJ\n",poww);
	return 0;
}

推荐题目

T1小凯的数字
T2【SDOI2016】排列计数
相信做完这两道题后可以让大家对逆元有一个更深刻的理解吧!

你可能感兴趣的:(———数论———,逆元,抽象代数,线性代数)