逆元与取模意义下的除法

逆元是什么

百度百科:

逆元素是指一个可以取消另一给定元素运算的元素,在数学里,逆元素广义化了加法中的加法逆元和乘法中的倒数。

好吧,这是一个比较奇怪的概念。

不过这里我们不涉及到其他种类的逆元,仅讨论 乘法逆元

在对 \(p\) 取模意义下的除法

考虑一个题目:

如果设斐波那契数列第 \(i\) 项为 \(f_i\) ,给定两个数 \(a,b\),求 \(\dfrac{f_a}{b} \bmod p\) 的值

保证 \(p|f_a\)

这道题本来是很简单的,但是由于 \(f_a\) 的值可能会很大,所以需要在计算的过程中,就顺手取模

但问题就来了,如果现在 \(f_a \bmod p\) 的值,

但是 \(\dfrac{f_a \bmod p}{b} \bmod p\) 的值等于 \(\dfrac{f_a}{b} \bmod p\) 的值吗?

显然不行,举个例子: \(\dfrac{20}{2} \bmod 6=4\),但是 \(\dfrac{20 \bmod 6}{2} \bmod 6=1\)

所以这个方法是行不通的

这个方法最大的问题是什么呢?

最大的问题是在于虽然 \(f_a \bmod b =0\) ,但是 \(f_a \bmod p\) 极有可能不被 \(b\) 整除

所以说,我们需要想方设法不进行除法计算

回忆一下小学一年级学的倒数内容(笑):

倒数是指设一个数 \(x\) 与其相乘的积为 \(1\) 的数,记为 \(\dfrac{1}{x}\) ,过程为“乘法逆”,除了 \(0\) 以外的数都存在倒数,分子和分母相倒并且两个乘积是 \(1\) 的数互为倒数,\(0\) 没有倒数。

根据以上内容可以想到计算出 \(\dfrac{1}{b} \mod p\) 的值

而这个值,就是我们接下来要介绍的乘法逆元

乘法逆元,不就是倒数吗

正如题目所说,\(a\) 在模 \(p\) 意义下的乘法逆元就是 \(\dfrac{1}{a} \bmod p\)

\(a\) 的乘法逆元记为 \(a^{-1}\),根据逆元定义得 \(a \times a^{-1} \equiv 1 \pmod p\)

所以逆元就是符合 \(a \times a^{-1} \equiv 1 \pmod p\) 的值

逆元怎么求

逆元有多达 \(4\) 种求法,但基本上每种求法都有其不同的用途

费马小定理与快速幂

不会费马小定理的人,出门左转,OI-wiki

\(p\) 为素数,并且 \(\gcd(a,p)=1\),依据费马小定理得 \(a^{p-1} \equiv 1 \pmod p\)

\[\because a^{p-1} \equiv 1 \pmod{p}\\ \therefore a^{p-2} \times a\equiv 1 \pmod p\\ \because a^{-1} \times a \equiv 1 \pmod p\\ \therefore a^{p-2} \equiv a^{-1} \pmod p \]

依据上面的推断,只需要用快速幂求出 \(a^{p-2} \mod p\) 的值就好了

注意 :这种算法只适用于 \(p\) 为质数的情况

代码实现

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;

int qpow(int a,int b,int p)
{
    int ret=1;
    while(b)
    {
        if(b&1) ret=(ret*a)%p;
        b>>=1;
        a=(a*a)%p;
    }
    return ret;
}

int inv(int a,int p)
{
    return qpow(a,p-2,p);
}

扩展欧几里得

不会扩展欧几里得的读者,出门左转,欧几里得算法、扩展欧几里得与同余方程

求逆元本质上就是解 \(ax \equiv 1 \pmod p\) 这个方程

所以直接用扩欧来求就可以了

代码实现

void exgcd(int a,int b,int &x,int &y)
{
    if(!b) x=1,y=0;
    else exgcd(b,a%b,y,x),y-=a/b*x;
}

int inv(int a,int p)
{
    int x,y;
    exgcd(a,p,x,y);
    return (x%p+p)%p;
}

线性递推

方案一

显然 \(1^{-1}\equiv 1\pmod p\)

对于 \(i^{-1}\),令 \(k=\lfloor \dfrac{p}{i} \rfloor,j=p \bmod i\)

\(p=ki+j\)

所以 \(ki+j \equiv 0 \pmod p\)

两边同时乘以 \(i^{-1}\times j^{-1}\),得 \(kj^{-1}+i^{-1}\equiv 0 \pmod p\)

\(i^{-1}\equiv -kj^{-1} \pmod p\)

带入 \(k=\lfloor \dfrac{p}{i} \rfloor,j=p \bmod i\) ,得 \(i^{-1}\equiv -\lfloor \dfrac{p}{i} \rfloor (p \bmod i)^{-1} \pmod p\)

所以就可以得到递推公式:

\[inv_i \equiv \begin{cases} 1 & i=1,\\ -\lfloor \dfrac{p}{i} \rfloor inv_{p \bmod i} & \text{otherwises.} \end{cases} \pmod p \]

方案二

由于 \(\dfrac{1}{i!}\times (i-1)!=\dfrac{1}{i}\),所以 \(i^{-1}=(i!)^{-1}\times (i-1)!\)

因为 \(\dfrac{1}{(i+1)!} \times (i+1)=\dfrac{1}{i!}\) ,所以 \((i!)^{-1}=((i+1)!)^{-1}\times (i+1)\)

所以进行两组递推,令 \(inv_i=i^{-1},inv'_i=(i!)^{-1}\)

所以先用其他方法求出 \(inv'_n\) 的值,然后递推

\[\begin{aligned} inv'_i &=(i+1)\times inv'_{i+1}\\ inv_i &=(i-1)!\times inv'_i \end{aligned} \]

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
typedef long long ll;
ll qpow(ll a,ll b,ll p)
{
    ll ret=1;
    while(b)
    {
        if(b&1) ret=(ret*a)%p;
        b>>=1;
        a=(a*a)%p;
    }
    return ret;
}

ll inv(ll a,ll p)
{
    return qpow(a,p-2,p);
}
const int MAXN=20000529;
ll n,p,ivf[MAXN],fac[MAXN];

int main()
{
    scanf("%lld%lld",&n,&p);
    fac[0]=1;
    for(int i=1;i<=n;i++) fac[i]=(fac[i-1]*i)%p;
    ivf[n]=inv(fac[n],p);
    for(int i=n-1;i>=1;i--) ivf[i]=((i+1)*ivf[i+1])%p;
    for(int i=1;i<=n;i++) printf("%lld\n",(fac[i-1]*ivf[i])%p);
    return 0;
}

以上代码用来快速幂来求 \(inv'_n\),但实际上也可以采用扩欧

线性求任意 \(n\) 个数的逆元

由于以上方法只适用于单个数的逆元以及 \(1\)\(n\) 的逆元,

那么再给一道例题:给定长度为 \(n\) 的数组 \(a\) 以及 \(p\)\(\forall 1 \leq i \leq n,1 \leq a_i<p\)),求数组里所有数的逆元

这里如果依靠上面的方法,只能将 \(n\) 个数一一算出,总复杂度为 \(n\log p\)

如果 \(n,p\) 比较大的情况下就会超时

能不能将数组进行整体处理呢?

答案当然是可以

\(s_i=\prod\limits_{j=1}^ia_j\)

\(s_{i-1}\times s_i^{-1}=(\prod\limits_{j=1}^{i-1}a_j)\times \dfrac{1}{\prod\limits_{j=1}^ia_j}=\dfrac{1}{a_i}=a_i^{-1}\)

同时 \(s^{-1}_i\times a_i=\dfrac{1}{\prod\limits_{j=1}^ia_j}\times a_i=\dfrac{1}{\prod\limits_{j=1}^{i-1}a_j}=s_{i-1}^{-1}\)

通过这两个公式就可以在 \(O(n+\log p)\) 的复杂度内计算完了

s[0]=1;
for(int i=1; i<=n; i++) s[i]=s[i-1]*a[i]%p;
sv[n]=qpow(s[n],p-2);
// 当然这里也可以用 exgcd 来求逆元,视个人喜好而定.
for(int i=n; i>=1; i--) sv[i-1]=sv[i]*a[i]%p;
for(int i=1; i<=n; i++) inv[i]=sv[i]*s[i-1]%p;

最开始的问题

最开始的问题在这时早已迎刃而解

\(\dfrac{f_a}{b} \bmod p\) 的值就是 \(f_a \times b^{-1} \bmod p\) 的值

可以在 \(O(a+\log p)\) 的时间复杂度解决这个问题

(如果使用矩阵快速幂加速的话,可以在 \(O(\log a+\log p)\) 的时间复杂度解决)

posted @ 2022-03-15 22:43  yhang323  阅读(159)  评论(0编辑  收藏  举报