所谓乘法逆元,就是两个整数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互质).
这样是不是清楚多了?如果不是,左转百度百科.
(注:以下算法时间复杂度均为求一个数逆元的时间级)
时间复杂度: 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
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?尽管在其他板块这是个优秀的复杂度,但在数论方面太菜了 显然还能更优.
下面介绍另一种方法.
时间复杂度: 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
#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;
}
更多有关扩欧的例题:青蛙的约会.
时间复杂度: 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
( 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
#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=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
#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】排列计数
相信做完这两道题后可以让大家对逆元有一个更深刻的理解吧!