基本概念
所谓乘法逆元,就是两个整数a和x相乘再用一个(非1正整数)数p对它们取模,若取模后所得的值等于1,那么x和a在模p条件下互为乘法逆元.
用同余方程表达即:
a
∗
x
≡
1
(
m
o
d
p
)
{a*x≡1(mod~p)}
a∗x≡1(mod p),
用一般方程表达为:
a
∗
x
−
k
∗
p
=
1
,
(
k
∈
z
)
{a*x-k*p=1,(k∈z)}
a∗x−k∗p=1,(k∈z).
(
a
存
在
逆
元
时
有
一
充
要
条
件
:
g
c
d
(
a
,
p
)
=
1
即
a
,
p
互
质
)
{(a存在逆元时有一充要条件:gcd(a,p)=1即a,p互质)}
(a存在逆元时有一充要条件:gcd(a,p)=1即a,p互质).
这样是不是清楚多了?如果不是,左转百度百科.
(注:以下算法时间复杂度均为求一个数逆元的时间级)
逆元求法
1.费马小定理+快速幂
时间复杂度:
O
(
l
o
g
n
)
{O(logn)}
O(logn)(总
O
(
n
∗
l
o
g
n
)
{O(n*logn)}
O(n∗logn))
条件限制:
p
{p}
p为质数.
(注:若p为合数,也可以用快速幂实现,不过要用上欧拉定理(小费马的一般形式),但用它来求逆元的实用性不大(要用欧拉筛
O
(
n
)
{O(n)}
O(n).)
*(欧拉定理:若a、p互素,则有:
(
φ
(
p
)
为
[
1
,
p
−
1
]
的
整
数
中
与
p
互
质
的
数
的
个
数
{φ(p)为[1,p-1]的整数中与p互质的数的个数}
φ(p)为[1,p−1]的整数中与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)=x∗a,
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);}
对于整数a和质数p,若p∣a,那么ap≡a(mod p);
否
则
:
a
p
−
1
≡
1
(
m
o
d
p
)
{否则:a^{p-1}≡1(mod~p)}
否则:ap−1≡1(mod p).
所以将该定理变形得
a
p
−
1
−
k
∗
p
=
1
(
k
∈
z
)
{a^{p-1}-k*p=1(k∈z)}
ap−1−k∗p=1(k∈z)
带入
a
∗
x
−
k
∗
p
=
1
,
(
k
∈
z
)
{a*x-k*p=1,(k∈z)}
a∗x−k∗p=1,(k∈z).得
x
=
a
p
−
2
{x=a^{p-2}}
x=ap−2.
然后由于题中自带的模数都比较大,所以你懂的 快速幂码起!
例题:【模板】乘法逆元
#include<bits/stdc++.h>
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(n∗ln n))
条件限制:似乎整数就行.
由这个方程:
a
∗
x
−
k
∗
p
=
1
,
(
k
∈
z
)
{a*x-k*p=1,(k∈z)}
a∗x−k∗p=1,(k∈z),令
k
=
−
y
,
(
y
∈
z
)
{k=-y,(y∈z)}
k=−y,(y∈z)
然后用求二元一次方程的方法,用扩展欧几里得算法求得:
a
∗
x
+
p
∗
y
=
1
,
(
k
∈
z
)
{a*x+p*y=1,(k∈z)}
a∗x+p∗y=1,(k∈z)一组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,m−1]即可得到符合条件的解.
证明:
假如
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=k∗p+r(k是a除以p的商,r是余数),并且我们已经求出了
p
∗
x
+
r
∗
y
=
1
{p*x+r*y=1}
p∗x+r∗y=1的一组解(x0,y0).
p
∗
x
0
+
(
a
−
k
∗
p
)
∗
y
0
=
1
{p*x0+(a-k*p)*y0=1}
p∗x0+(a−k∗p)∗y0=1
a
∗
x
1
+
p
∗
y
1
=
1
{a*x1+p*y1=1}
a∗x1+p∗y1=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}
p∗x0+a∗y0−k∗p∗y0=p∗(x0−k∗y0)+a∗y0=a∗x1+p∗y1
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=y0;y1=x0−k∗y0=x0−(a/p)∗y0;
那么(x1,y1)就是
a
∗
x
+
p
∗
y
=
1
{a*x+p*y=1}
a∗x+p∗y=1的一组解.
虽然上一题还是不能过,但它可以过这个题:(第一种方法只有20分)
【NOIPS2012】同余方程
#include<bits/stdc++.h>
#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}
1−1≡1(modp)
然后设
p
=
k
∗
i
+
r
,
(
1
<
r
<
i
<
p
)
{p=k*i+r,(1<r<i<p)}
p=k∗i+r,(1<r<i<p)
(
k
是
p
/
i
的
商
,
r
是
余
数
{k是p/i的商,r是余数}
k是p/i的商,r是余数)。
再将这个式子放到
(
m
o
d
p
)
{\pmod p}
(modp)意义下就会得到:
k
∗
i
+
r
≡
0
(
m
o
d
p
)
①
{k*i+r \equiv 0 \pmod p ①}
k∗i+r≡0(modp)①
然后乘上
i
−
1
,
r
−
1
{i^{-1} ,r^{-1}}
i−1,r−1就可以得到:
k
∗
r
−
1
+
i
−
1
≡
0
(
m
o
d
p
)
{k*r^{-1}+i^{-1}\equiv 0 \pmod p}
k∗r−1+i−1≡0(modp)
i
−
1
≡
−
k
∗
r
−
1
(
m
o
d
p
)
{i^{-1}\equiv -k*r^{-1} \pmod p}
i−1≡−k∗r−1(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②}
i−1≡−⌊ip⌋∗(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,所以,在求出i−1之前,我们早已求出(pmodi)−1
因此用数组
n
y
[
i
]
{ny[i]}
ny[i]记录
i
−
1
{i^{-1}}
i−1(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}
i−1>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]=p−ip ∗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<bits/stdc++.h>
#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<<endl;//第二题输出(过不了)
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=m∗g∗(x2−x1)2/(4∗∣y2−y1∣)
针对向上的情况我们有:
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=m∗g∗[(y2−y1)+(x2−x1)2/(4∗∣y2−y1∣)].
(前两个应该高中及以上的都会推吧(小初的巨神表示不屑于此 ))
针对高度差为零的情况,我们只需要求一个斜抛运动的最小初速度就行了(也很好证的,所以我就不证了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(x2−x1),E=mg(x2−x1)/2.
最后用扩欧或小费马快速幂求逆元解决除法即可.
#include<bits/stdc++.h>
#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】排列计数
相信做完这两道题后可以让大家对逆元有一个更深刻的理解吧!