【算法笔记】乘法逆元

  • 本文总计约 8000 字,阅读大约需要 30 分钟
  • 警告!警告!警告!本文有大量的 LATEX 公式渲染,可能会导致加载异常缓慢!

前言

逆元可能是数论里面最最最最最重要的知识点了,因为大部分的 OI 题目里,都需要选手将答案对某个大质数(一般是 109+7 或者是 998244353)取模,而在取模的过程中,因为取模不满足除法分配率,所以在遇见除法的时候,不能直接相除

但这并不意味着我们对除法取模没有办法。而我们实现它的方法,就是使用乘法逆元来完成的。

题目引入

我们定义组合数 (nm) 代表从 n 个互不相同的元素中,选择其中 m 个,有多少种选法。

计算组合数的公式为:(nm)=n!m!(nm)!,其中 n! 代表 n 的阶乘,它的定义为 n!=12n

现在给你两个正整数 n,m,且 nm,求 (nm)998244353 取模后的值。

暴力解法

对于求组合数问题,有两种基本的算法:

  1. 通过组合数公式 (nm)=n!m!(nm)! 暴力硬算。int 不够开 long longlong long 不够写高精。复杂度为 Θ(n)

    然鹅如果 n107,写高精似乎会超时 QwQ。这也是为什么我们要让答案对 998244353 取模,根据同余定理,有 (amodc)(bmodc)=(ab)modc。所以,无论 n 再怎么大,取模后的结果也总是能被一个 int 变量存储下。大部分 OI 题里,答案对大数取模,目的就是让输出的答案不要过大毕竟写高精是很麻烦的一件事 QwQ

    但是我们会发现,计算组合数的过程中,需要使用除法。这里要强调一下:(ab)modc(amodcbmodc)modc!例如,102mod3=5mod3=2,但 10mod32mod3=12,两者显然不相等。所以,我们不能直接对除法运算取模

  2. 可以通过杨辉三角来计算组合数。根据公式 (nm)=(n1m)+(n1m1),我们可以存储先前已经算出来的组合数,然后递推地求出其它的组合数。

    时间复杂度是 Θ(n2),比暴力硬算的效率低,却可以绕过除法取模的问题。但当 n107 的时候,好像会超时 QwQ,所以我们只能放弃掉这个算法了。

乘法逆元

除法取模

正如上面所说,杨辉三角的算法时间复杂度太低,所以我们还是得回到 Θ(n) 的公式爆算中来。既然如此,我们就必须得直面除法取模的算法。

我们考虑最简单的情况,即 abc 取模:

abM(modc).(1.1)

我们在 (1.1) 式两侧同时乘上一个 b,那么上式变为:

aMb(modc).(1.2)

现在,让我们再考虑这个同余方程:

bx1(modc).(1.3)

假如我们能够解出方程 (1.3) 中的 x,再将其代入到 (1.2) 中,就有了:

axM(modc).(1.4)

同时联立 (1.1)(1.4),我们就可以得出:

abax(modc), which bx1(modc)(1.5)

也就是说,假如我们可以求出上面方程 3 中的 x 的值,我们就可以通过 (1.5) 式求出 abc 取模的结果了。

乘法逆元的概念

假如对于一个正整数 a,存在一个整数 x,使得同余方程 ax1(modp) 成立,那么我们称 xa 在模 p 意义下的逆元,记做 xa1(modp)

它的作用是什么……当然就是向上面一样,解决除法取模的问题。因为除法是乘法的逆运算,我们就可以认为,在模 p 的意义下,除以一个整数,和乘以这个整数在模 p 意义下的逆元,效果是完全相同的。

乘法逆元的性质

我们对上面的乘法逆元的概念有了基本的了解,并且还可以通过观察,简单地得到一些乘法逆元的性质:

  1. 在模 p 意义下,任意一个数的乘法逆元一定小于 p 本身。
    这个结论似乎很显然。因为我们不妨设 a 的乘法逆元为 x,且 x>p,那么一定有 a(xmodp)ax1(modp),所以 (xmodp)a 的乘法逆元,且 (xmodp)<p,所以 a 的乘法逆元小于 p

  2. 在模 p 意义下整数 a 存在逆元,当且仅当 pa
    不妨设 a1=x,则:

    ax1(modp).(2.1)

    稍微对 (2.1) 做一下变形,就有:

    ax=kp+1,kZ.(2.2)

    移项,得:

    ax+kp=1.(2.3)

    注意到这是一个关于 k,x 的二元一次不定方程。我们利用 Bézout 定理,可知,x 存在解,当且仅当 gcd(a,p)=1,即 ap,命题得证。

    同时,我们也可以得到另外一个结论:在模 p 的意义下,1,2,,p1 都存在逆元,当且仅当 p 是一个质数。

  3. p 是质数,那么在模 p 意义下,1 的逆元为 1(p1) 的逆元为 (p1),除此以外,2(p2) 之间,任何一个数和它的逆元一一对应

    这个结论又有一个高大上的名字,叫做 Wilson 定理:

    当且仅当 p 是一个质数时,(p1)!1(modp)

    证明如下(数学证明警告!):

    先证必要性,即 (p1)!1(modp) p 是一个质数:

    p 不是一个质数,存在正整数 a,b 使得 p=ab,显然,a<p1b<p1

    ab 时,(p1)!=12  a  b  (p1),所以 ab(p1)!

    a=b 时,(p1)!=12  a  2b  (p1).

    综上,此时 p(p1)!。所以必要性成立;

    再证充分性,即 p 是一个质数 (p1)!1(modp)

    显然 p=2,3 时,命题成立;

    对于任意质数 p5,令 A={2,3,,p2},对于任意 aA,令 B={a,2a,,(p1)a}

    任取 1x<yp1,那么 xaya,且 xa,yaB,由于 (yx)aB,所以 p(yaxa),所以对于任意 xa,yaBxaya(modp)

    所以 B 中任意两个元素模 p 不同余。

    C={b|b=xmodp,xB},则 C={1,2,3,,p1}。设 ax1(modp),则:

    x1,假设 x=1,则 axa1(modp),即 a=1A,与 aA 矛盾;

    同理,xp1

    xa,假设 x=a,则 a21(modp),可知 (a1)(a+1)0(modp),解得 a=1a=p1,即 aA,舍去。

    所以 aA 时,存在唯一的 xA,使得 ax1(modp),且任意两个 a,x 相互对应。

    因此充分性成立。

接下来,在了解了这 3 条性质之后,我们就要想想乘法逆元的求法了。

乘法逆元的求法

说了这么多,我们现在已经知道了模数,那么我们应该怎么求一个数 a 的逆元呢?

当然可以暴力解同余方程 ax1(modp),时间复杂度为 O(p)超时了别赖我 QwQ

我们能不能用一种更快的算法来求逆元呢?当然可以,有下面的这 3 种求法供你选择:

扩展 Euclid 算法

我们考虑 (2.3) 的不定方程,其中 xa 的逆元。

一说到不定方程,我们马上就应该想到:用扩欧啊!

于是,这段代码就此产生了:

//exgcd 求逆元
int inv(int a, int p, int & x, int & k) {
	if(p == 0) {
		x = 1;
		k = 0;
	}
	
	else {
		inv(p, a % p, k, x);
		k -= a / p * x;
	}
	
	return (x % p + p) % p;  //注意求出的逆元可能是负数,我们要将其变为正的
}

它的时间复杂度为 O(logn)

Fermat 小定理

Fermat 小定理是一个关于同余的问题,它的表述如下:

p 是质数且 ap,则 ap11(modp)

下面给出证明(数学证明警告!):

我们考虑一系列数 1,2,,p1,由于 p 是质数,所以它们均与 p 互质。

假如 ap,我们不妨设 a,2a,3a,,(p1)a,可以证明它们模 p 两两不同余(证明方法可以参照上面 Wilson 定理的证明),所以它们对 p 取模后,调整顺序,依旧为 1,2,,p1

a2a3a  (p1)a123  (p1)(modp)

ap1(p1)!(p1)!(modp),两边同时除以 (p1)!,得 ap11(modp)

当然,不看证明也是可以的。如果是要求逆元的话,我们只需要将这个定理小小地变形,则有 aap21(modp)。于是有 a1ap2(modp),我们可以通过快速幂来求出 ap2p 取模的结果了,时间复杂度依旧为 Θ(logn)

线性求逆元

O(logn) 的算法似乎非常高效了,但是还不够高效。回到题目引入中的问题,如果 m107 的话,就意味着我们需要至少 m 次计算阶乘中每一个数的逆元,每次都是 O(logm),总复杂度为 O(mlogm),在 m107 的时候可能会被卡掉。

我们依旧需要尝试减少重复计算同一个逆元的次数,将其时间复杂度变为 O(1)。如下:

有一个很显然的结论:

p0(modp)(3.1).

因为商乘除数加余数等于被除数,所以有

p=xpx+(pmodx)(3.2).

代入 (3.1) 式,移项,得:

xpx(pmodx)(modp)(3.3).

两边同乘 (pmodx)1,得:

(pmodx)1pxx1(modp)(3.4).

根据乘法逆元的定义,我们有

x1p(pmodx)1px(modp)(3.5).

(3.5) 式可知,如果我们已经求出了 1pmodx 的逆元,那么我们就可以通过 O(1) 递推转移过来 x 的逆元,这个过程中,求出 1n 的逆元的时间复杂度是线性的 Θ(n),而非 O(nlogn),这样,就可以通过 n107 大数据了。

代码如下:

int inv[p];
inv[1] = 1;  //初始化 1 的逆元为 1

for(int i = 2; i <= p; ++i) {
	inv[i] = (long long)(p - p / i) * (p % i) % p;  //转移逆元
}

当然,虽然第三种算法的效率更高,但是它不大能用来求单个数的逆元,而且还浪费了 O(n) 的辅助数组的空间复杂度。所以,选择哪种算法,要看具体题目情景里面是如何要求的。

有理数取模

那么,有了乘法逆元之后,我们就可以将取模的运算从整数推广到有理数

假如一个有理数 q=ab,我们要求 qmodp 的值,其中 pb。根据 (1.5) 式,我们不难得出 qab1(modp)

也就是说,我们可以将取模运算推广到有理数,这样,就可以将其用于求概率或者数学期望时候的取模了。

例题

本题目列表会持续更新。

参考文献

posted @   CaO氧化钙  阅读(247)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示