模逆元

本文介绍模意义下乘法运算的逆元,并讨论它的常见求解方法.

基本概念

非零实数 a∈Ra\in\mathbf R 的乘法逆元就是它的倒数 a−1a^{-1}.类似地,数论中也可以定义一个整数 aa 在模 mm 意义下的逆元 a−1 mod ma^{-1}\bmod m,或简单地记作 a−1a^{-1}.这就是 模逆元(modular multiplicative inverse),也称作 数论倒数.

逆元

对于非零整数 a,ma,m,如果存在 bb 使得 ab≡1(modm)ab\equiv 1\pmod m,就称 bb 是 aa 在模 mm 意义下的 逆元(inverse).

这相当于说,bb 是线性同余方程 ax≡1(modm)ax\equiv 1\pmod m 的解.根据 线性同余方程 的性质可知,当且仅当 gcd⁡(a,m)=1\gcd(a,m)=1,即 a,ma,m 互素时,逆元 a−1 mod ma^{-1}\bmod m 存在,且在模 mm 的意义下是唯一的.

单个逆元的求法

利用扩展欧几里得算法或快速幂法,可以在 O(log⁡m)O(\log m) 时间内求出单个整数的逆元.

扩展欧几里得算法

求解逆元,就相当于求解线性同余方程.因此,可以使用 扩展欧几里得算法 在 O(log⁡min⁡{a,m})O(\log\min\{a,m\}) 时间内求解逆元.同时,由于逆元对应的线性方程比较特殊,可以适当地简化相应的步骤.

参考实现

C++

// Extended Euclidean algorithm.
void ex_gcd(int a, int b, int& x, int& y) {
  if (!b) {
    x = 1;
    y = 0;
  } else {
    ex_gcd(b, a % b, y, x);
    y -= a / b * x;
  }
}

// Returns the modular inverse of a modulo m.
// Assumes that gcd(a, m) = 1, so the inverse exists.
int inverse(int a, int m) {
  int x, y;
  ex_gcd(a, m, x, y);
  return (x % m + m) % m;
}

Python

# Extended Euclidean algorithm.
def ex_gcd(a, b):
    if b == 0:
        return 1, 0
    else:
        x1, y1 = ex_gcd(b, a % b)
        x = y1
        y = x1 - (a // b) * y1
        return x, y


# Returns the modular inverse of a modulo m.
# Assumes that gcd(a, m) = 1, so the inverse exists.
def inverse(a, m):
    x, y = ex_gcd(a, m)
    return (x % m + m) % m

这一算法适用于所有逆元存在的情形.

快速幂法

这一方法主要适用于模数是素数 pp 的情形.此时,由 费马小定理 可知对于任意 a⊥pa\perp p 都有

a⋅ap−2=ap−1≡1(modp).a\cdot a^{p-2} = a^{p-1} \equiv 1 \pmod p.

根据逆元的唯一性可知,逆元 a−1 mod pa^{-1}\bmod p 就等于 ap−2 mod pa^{p-2}\bmod p,因此可以直接使用 快速幂 在 O(log⁡p)O(\log p) 时间内计算:

参考实现

C++

// Binary exponentiation.
int pow(int a, int b, int m) {
  long long res = 1, po = a;
  for (; b; b >>= 1) {
    if (b & 1) res = res * po % m;
    po = po * po % m;
  }
  return res;
}

// Returns the modular inverse of a prime modulo p.
int inverse(int a, int p) { return pow(a, p - 2, p); }

Python

# Returns the modular inverse of a prime modulo p.
# Use built-in pow function.
def inverse(a, p):
    return pow(a, p - 2, p)

当然,理论上,这一方法可以利用 欧拉定理 推广到一般的模数 mm 的情形,即利用 aφ(m)−1 mod ma^{\varphi(m)-1}\bmod m 计算逆元.但是,单次求解 欧拉函数 φ(m)\varphi(m) 并不容易,因此该算法在一般情况下效率不高.

多个逆元的求法

有些场景下,需要快速处理出多个整数 a1,a2,⋯ ,ana_1,a_2,\cdots,a_n 在模 mm 意义下的逆元.此时,逐个求解逆元,总共需要 O(nlog⁡m)O(n\log m) 的时间.实际上,如果将它们统一处理,就可以在 O(n+log⁡m)O(n+\log m) 的时间内求出所有整数的逆元.

考虑序列 {ai}\{a_i\} 的前缀积:

S0=1, Si=aiSi−1, i=1,2,⋯ ,n.S_0 = 1,~ S_i = a_iS_{i-1},~ i=1,2,\cdots,n.

只要每个 aia_i 都与 mm 互素,它们的乘积 SnS_n 就与 mm 互素.因此,可以通过前文所述算法求出 Sn−1 mod mS_n^{-1}\bmod m 的值.因为乘积的逆元就是逆元的乘积,所以,从 Sn−1S_n^{-1} 出发,反向遍历序列就能求出每个 SiS_i 的逆元:

Si−1−1=aiSi−1 mod m, i=n,n−1,⋯ ,1.S_{i-1}^{-1} = a_iS_i^{-1} \bmod m,~ i = n,n-1,\cdots,1.

由此,单个 aia_i 的逆元可以通过下式计算:

ai−1=Si−1Si−1 mod m, i=1,2,⋯ ,n.a_i^{-1} = S_{i-1}S_i^{-1} \bmod m,~ i = 1,2,\cdots,n.

参考实现如下:

参考实现

C++

// Returns the modular inverses for each x in a modulo m.
// Assume x mod m exists for each x in a.
std::vector<int> batch_inverse(const std::vector<int>& a, int m) {
  int n = a.size();
  std::vector<int> prod(n);
  long long s = 1;
  for (int i = 0; i < n; ++i) {
    // prod[i] = product of a[0...i-1]; prod[0] = 1.
    prod[i] = s;
    s = s * a[i] % m;
  }
  // s = product of all elements in a.
  s = inverse(s, m);
  std::vector<int> res(n);
  for (int i = n - 1; i >= 0; --i) {
    res[i] = s * prod[i] % m;
    s = s * a[i] % m;
  }
  return res;
}

Python

# Returns the modular inverses for each x in a modulo m.
# Assume x mod m exists for each x in a.
def batch_inverse(a, m):
    n = len(a)
    prod = [0] * n
    s = 1
    for i in range(n):
        # prod[i] = product of a[0...i-1]; prod[0] = 1.
        prod[i] = s
        s = s * a[i] % m
    # s = product of all elements in a.
    s = inverse(s, m)
    res = [0] * n
    for i in reversed(range(n)):
        res[i] = s * prod[i] % m
        s = s * a[i] % m
    return res

算法中,只求了一次单个元素的逆元,因此总的时间复杂度是 O(n+log⁡m)O(n+\log m) 的.

线性时间预处理逆元

如果要预处理前 nn 个正整数在素数模 pp 下的逆元,还可以通过本节将要讨论的递推关系在 O(n)O(n) 时间内计算.这一方法常用于组合数计算中前 nn 个正整数的阶乘的倒数的预处理.

对于 1<i<p1< i < p 的正整数 ii,考察带余除法:

p=⌊pi⌋i+(p mod i).p = \left\lfloor \dfrac{p}{i} \right\rfloor i + (p\bmod i).

将该等式对素数 pp 取模,就得到

0≡⌊pi⌋i+(p mod i)(modp).0 \equiv \left\lfloor \dfrac{p}{i} \right\rfloor i + (p\bmod i) \pmod p.

将等式两边同时乘以 i−1(p mod i)−1i^{-1}(p\bmod i)^{-1} 就得到

i−1≡−⌊pi⌋(p mod i)−1(modp).i^{-1} \equiv - \left\lfloor \dfrac{p}{i} \right\rfloor (p\bmod i)^{-1} \pmod p.

这就是用于线性时间递推求逆元的公式.由于 p mod i<ip\bmod i < i,这一公式将求解 i−1 mod pi^{-1}\bmod p 的问题转化为规模更小的问题 (p mod i)−1 mod p(p\bmod i)^{-1}\bmod p.因此,从 1−1 mod p=11^{-1}\bmod p=1 开始,对每个 ii 顺次应用该公式,就可以在 O(n)O(n) 时间内获得前 nn 个整数的逆元.

参考实现如下:

参考实现

C++

// Precomputes modular inverses of all integers from 1 to n modulo prime p.
std::vector<int> precompute_inverses(int n, int p) {
  std::vector<int> res(n + 1);
  res[1] = 1;
  for (int i = 2; i <= n; ++i) {
    res[i] = (long long)(p - p / i) * res[p % i] % p;
  }
  return res;
}

Python

# Precomputes modular inverses of all integers from 1 to n modulo prime p.
def precompute_inverses(n, p):
    res = [0] * (n + 1)
    res[1] = 1
    for i in range(2, n + 1):
        res[i] = (p - p // i) * res[p % i] % p
    return res

这一算法只适用于模数是素数的情形.对于模数 mm 不是素数的情形,无法保证递推公式中得到的 m mod im\bmod i 仍然与 mm 互素,因而递推所需要的 (m mod i)−1(m\bmod i)^{-1} 可能并不存在.一个这样的例子是 m=8,i=3m=8,i=3.此时,m mod i=2m\bmod i = 2,不存在模 mm 的逆元.

另外,得到该递推公式后,一种自然的想法是直接递归求解任意一个数 aa 的逆元.每次递归时,都利用递推公式将它转化为更小的余数 p mod ap\bmod a 的逆元,直到余数变为 11 时停止.目前尚不清楚这样做的复杂度1,因此,推荐使用前文所述的常规方法求解.

习题

参考资料与注释

Footnotes

  1. riteme 在知乎上的回答 中指出,这样做理论上已知的复杂度的上界是 O(p1/3+ε)O(p^{1/3+\varepsilon}),而在实际随机数据中的表现接近于 O(log⁡p)O(\log p). ↩

练习

本文内容来自 OI Wiki《模逆元》;作者与修订历史见原文。AIOJ 对排版、站内链接和资源路径做了格式适配。

内容依 CC BY-SA 4.0 与 SATA 许可共享;附加条款以原文声明为准。感谢 OI Wiki 作者与贡献者;请按 SATA 约定前往 OI Wiki 仓库 Star 支持。