乘法逆元及其 python 求解法

定义

  乘法逆元是群论中的一个概念,它也在各类算法题中重现,是最好需要掌握的一项技巧。在同余中,乘法逆元的简单定义是,满足下式的 xxx 称为 aaannn 的乘法逆元。
ax≡1(mod⁡n)(1)ax\equiv1 (\operatorname{mod}n)\tag 1ax1(modn)(1) 然而这个乘法逆元 xxx 还不一定是存在的。aaannn 的乘法逆元存在的充要条件是,aaannn 互质。
  这个乘法逆元的作用就是,求 y/a(mod⁡n)y/a(\operatorname{mod} n)y/a(modn) 可以转化为求 yx(mod⁡n)yx(\operatorname{mod} n)yx(modn)

举一个实用的例子,求组合数 C(n,k)=n!k!(n−k)!C(n,k)=\cfrac{n!}{k!(n-k)!}C(n,k)=k!(nk)!n! 对一个很大的质数 ppp 的模。显然,如果只是一个 (n,k)(n,k)(n,k) 数据,暴力求一下也就行了。但如果给你很多很多 (n,k)(n,k)(n,k) 的数据,时间复杂度就不可接受了。此时乘法逆元的作用就表现出来:
假设我们预先知道 nnn 的上界,那我们就可以用 O(n)O(n)O(n) 的复杂度构建fact数组,其中fact[i]的值是 i!mod⁡pi!\operatorname{mod} pi!modp。另外,再用 O(nlog⁡p)O(n\log p)O(nlogp) 的复杂度构建invs数组,其中invs[i]的值是 i!i!i!ppp 的乘法逆元。
在经过 O(nlog⁡p)O(n\log p)O(nlogp) 复杂度的预处理之后,求解组合数就变成了 O(1)O(1)O(1) 复杂度的事情。即C(n,k)=fact[n] * invs[k] * invs[n - k] % p

gcd 算法

  先介绍一下求解逆元的前置知识。

辗转相除法求最大公因数

  相信大家在学习一门编程语言的时候,接触过求解最大公因数的算法。最出名的莫过于辗转相除法。记 a,ba,ba,b 的最大公因数为 gcd⁡(a,b)\operatorname{gcd}(a,b)gcd(a,b),那么 gcd⁡(a,b)=gcd⁡(b,amod⁡b)\operatorname{gcd}(a,b)=\operatorname{gcd}(b,a\operatorname{mod}b)gcd(a,b)=gcd(b,amodb)
  当时你大概没有考虑过这个算法的时间复杂度是多少。实际上,使用辗转相除法求解 a,ba,ba,b 的最大公因数,时间复杂度是 O(log⁡(max⁡(a,b)))O(\log(\max(a,b)))O(log(max(a,b)))

拓展 gcd 算法

  先要用到拓展 gcd算法。这个算法的目标是,对于任意的正整数 a,ba,ba,b 找到 x,yx,yx,y 满足:
ax+by=gcd⁡(a,b)(2)ax+by=\operatorname{gcd}{(a,b)}\tag 2ax+by=gcd(a,b)(2)   注意到当 b=0b=0b=0 时,上式化为 ax+by=bax+by=bax+by=b,那么就找到了 x=0,y=1x=0,y=1x=0,y=1
  而当 b≠0b\neq 0b=0 时,假设有一个 x^,y^\hat x,\hat yx^,y^ 满足 bx^+(amod⁡b)y^=gcd⁡(b,amod⁡b)(3)b\hat x+(a\operatorname{mod}b)\hat y=\operatorname{gcd}(b,a\operatorname{mod}b)\tag3bx^+(amodb)y^=gcd(b,amodb)(3) 根据辗转相除法我们知道 (2)(3)(2)(3)(2)(3) 两式的右边是相等的,因此得到 ax+by=bx^+(amod⁡b)y^(3)ax+by=b\hat x+(a\operatorname{mod}b)\hat y\tag 3ax+by=bx^+(amodb)y^(3) 其中,令 a=kb+t(k,t∈Z,0≤t<b)a=kb+t(k,t\in\mathbb Z,0\leq t<b)a=kb+t(k,tZ,0t<b),那么 (3)(3)(3) 又可以化为 kbx+tx+by=bx^+ty^(4)kbx+tx+by=b\hat x+t\hat y\tag{4}kbx+tx+by=bx^+ty^(4) 稍微整理一下,可得 b(kx+y−x^)+t(x−y^)=0(5)b(kx+y-\hat x)+t(x-\hat y)=0\tag 5b(kx+yx^)+t(xy^)=0(5)   因此,假设我们已经求得了 x^,y^\hat x,\hat yx^,y^,那么 x=y^x=\hat yx=y^y=x^−kx=x^−y^(a//b)y=\hat x-kx=\hat x-\hat y(a//b)y=x^kx=x^y^(a//b) 就是我们想要一组 (x,y)(x,y)(x,y) 的值。其中 a//b=⌊a/b⌋(a,b>0)a//b=\lfloor a/b\rfloor(a,b>0)a//b=a/b(a,b>0)
  由拓展 gcd 算法中,求解 (x,y)(x,y)(x,y) 和求 gcd⁡(a,b)\operatorname{gcd}(a,b)gcd(a,b) 是同步的,辗转相除法求解 gcd⁡(a,b)\operatorname{gcd}(a,b)gcd(a,b) 的终止条件也是 b=0b=0b=0。因此拓展 gcd 算法的时间复杂度也应该是 O(log⁡(max⁡(a,b)))O(\log(\max(a,b)))O(log(max(a,b))),这个 gcd 在拓展算法中起到一个引路的作用。

求解模素数的乘法逆元

  有了上面的基础,就可以求解模素数的乘法逆元了。假设 nnn 是一个正素数,a>0a>0a>0a≠na\neq na=n,那我们可以在 O(log⁡(min⁡(a,n)))O(\log(\min(a,n)))O(log(min(a,n))) 的时间复杂度内求得满足 ax+ny=gcd⁡(a,n)=1ax+ny=\operatorname{gcd}(a,n)=1ax+ny=gcd(a,n)=1(x,y)(x,y)(x,y)。式子两边同时对 nnn 去模,得到 ax≡1(mod⁡n)ax\equiv 1(\operatorname{mod}n)ax1(modn)。因此,通过拓展 gcd 算法求得的 xxx 就是我们想要的乘法逆元。
  附带 python 代码一份,用于求 bbbkkk 的乘法逆元。

def extended_gcd(a, b):
    if a == 0:
        return b, 0, 1
    gcd, x1, y1 = extended_gcd(b % a, a)
    x = y1 - (b // a) * x1
    y = x1
    return gcd, x, y

def mod_inverse(b, k):
    gcd, x, _ = extended_gcd(b, k)
    if gcd != 1:
        raise ValueError(f"No modular inverse for {b} mod {k}")
    return x % k
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值