乘法逆元
乘法逆元的定义及用途,以及求逆元的三种算法:扩展欧几里得、费马小定理、递推求逆元。
乘法逆元
首先,数学上的乘法逆元就是指直观的倒数,即
这里我们讨论关于取模运算的乘法逆元,即对于整数 a * x % b == 1
。
在算法竞赛中,经常会遇到求解数据很大,则输出模
举个例子:求3 * 6 / 3
对 7
取模的结果。我们直接算出3 * 6 / 3
的结果是6
,对7
取模得最终答案是
6
。
但我们通常面对的问题是中间结果超过int
甚至long long
的范围,而不得不在每一步基于同余理论取模,我们用这个例子尝试一下:
还是求 3 * 6 / 3 % 7
第一步:3 * 6 == 18
,18 % 7 == 4
第二步:4
这个中间结果再做 4 / 3
无法整除,就无法进行下去了。
但我们可以求出除数 3
关于模数7
的逆元
5
(根据逆元定义,5
符合
3 * 5 % 7 == 1
),从而,用乘以5
代替除以3
。
上述第二步除法变乘法:
4 * 5 == 20
,20 % 7 == 6
从而也计算出了正确的结果 6
。
乘法逆元的作用就是:
设
对于
在有些问题中,无法计算最终值很大的
故而我们需要一个算法求 除数 的 取模逆元 ,从而在四则运算取模的任务中,用逆元将除法转为乘法。
方法1:扩展欧几里得
先暂时将逆元的事放一放,来看下扩展欧几里得。
首先大多入门选手知道求两个数最大公约数的算法,即辗转相除:
1 | int GCD(int a, int b) {return b ? GCD(b, a % b) : a} |
扩展欧几里得算法则是求
设两个式子
由欧几里得算法知
所以
而
其中
所以有
展开移项得:
根据系数对应关系,可以设
递归地用
类似 GCD()
的递归终点,当扩展欧几里得算法ExGCD()
的
就能得到
1 | typedef long long LL; |
扩展欧几里得求逆元
了解了扩展欧几里得,我们来看它与乘法逆元的关系。
- 逆元:
关于 模 的逆元 整数 满足 - 扩展欧几里得:求方程
的一组可行解
逆元的
设

求出
1 | int ExGcdInv(int a, int b) |
时间复杂度:大约O(logn)
(斐波那契复杂度)。
适用范围:存在逆元即可求,适用于个数不多但模数b
很大的时候,最常用、安全的求逆元方式。
方法2:费马小定理
除了扩展欧几里得,还有另一个方法可以求逆元。
费马小定理:对于整数
快速幂取模
快速幂取模大家应该也学过了,这里复习一下:
求x ^ n % MOD
, n
很大时需要用折半的思想。如下所示求2^15
:
1 | 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 |
可以看到,两两结合的时候,如果数字个数是奇数就会有“零头”,把零头存入ret
,最终结果就是256*2*4*16
。
1 | int PowMod(int a, int n, int mod) |
费马小定理求逆元
上文费马小定理的式子等价于
显然
求逆元,就用 b-2
和 b 代替 快速幂取模中的 n
和 mod
:
1 | int FermatInv(int a, int b) |
时间复杂度:大约O(log b)
。
适用范围:一般在模数 b
是质数的时候。
方法3:递归/递推求逆元
求
设 $k = b / a
=>
=>
=>
将
从而我们得到了由
用递归方式计算的话,由于
1 | LL Inv(LL a, LL b) |
时间复杂度:大约O(log b)
适用范围:
逆元打表
基于递推方法,就可以打表
1 | int invList[mod + 10]; |
时间复杂度:显而易见。
适用场景:频繁调用不同数的逆元。