跳转至

第 81 章 快速幂与逆元

配套例题:BISHI64 【模板】快速幂Ⅰ ‖ 模小整数、BISHI65 【模板】分数取模、 BISHI74 【模板】非质模数下的乘法逆元 来源:S3 day6 pow.cpp;day9《数论》(rxz)第 64–73 页「除法 / 逆元」、逆元.cpp; day9 resource《数论与信息学竞赛》(胡渊鸣)第 15–17 页、《数论》(邹雨恒)第 15–20 页、 《数论》(李有为)第 13–15 页;day10《NOIP 组合数学、数论选讲》 前置80-数论基础46-位运算

rxz 的课件用了整整十页铺垫「模意义下的除法」这件事:

为什么我迟迟不讲模意义下的除法呢?想一想,有理数允许分数的存在,模意义下还允许吗? \((5/3)\times 12\),尽管开始出现了分数(这是不被允许的),但是后来乘上 12, 结果仍然是正整数,因此这个式子在模意义下是合法的。这个问题如何解决呢?

答案是乘法逆元:在模意义下,「除以 \(b\)」被重新定义为「乘以 \(b^{-1}\)」。 而求逆元最常用的手段又是快速幂。这两件事因此绑在一起讲。

本章的 Python 核心结论,一句话

模幂用内置 pow(a, b, m),逆元用 pow(a, -1, m)。一行都不要手写。


81.1 快速幂

原理:指数的二进制拆分

要算 \(a^b\),把 \(b\) 写成二进制 \(b = \sum_i c_i 2^i\)\(c_i \in \{0,1\}\)):

\[a^b = \prod_{c_i = 1} a^{2^i}\]

\(a^{2^0}, a^{2^1}, a^{2^2}, \ldots\) 可以逐次平方得到。于是只要 \(O(\log b)\) 次乘法。

S3 day6 的 pow.cpp 是递归写法:

int pow(int x,int p)
{
    if(p==0) return 1;
    if(p==1) return x;

    int a=pow(x,p/2);
    if(p%2==0) return a*a;
    else return a*a*x;
}

对应的迭代版(教学用,实战请看 81.2):

# [片段] 教学用手写快速幂 —— 理解原理即可,实战一律用内置 pow
def qpow(a, b, m):
    """a^b mod m,O(log b) 次模乘。"""
    res = 1
    a %= m
    while b:
        if b & 1:                # 当前二进制位是 1,累乘进答案
            res = res * a % m
        a = a * a % m            # a 自乘,对应下一个二进制位
        b >>= 1
    return res % m               # ★ 最后再模一次,处理 m == 1 的退化情况

m == 1 是快速幂最经典的坑:模 1 意义下一切都是 0, 但 res 初始化成 1、且 \(b=0\) 时循环一次都不进,会错误返回 1。 上面代码最后那个 % m 就是在补这个洞。内置 pow 没有这个问题pow(1, 0, 1) == 0

快速幂的推广

快速幂的本质是「结合律 + 二分」,所以凡是满足结合律的运算都能这么加速:

运算 快速幂形式 用途
整数乘法 \(a^b \bmod m\) 本章
矩阵乘法 \(A^n\) 线性递推加速(85 章
加法(快速乘) \(a \times b\) 用加法实现 C++ 防溢出,见 81.3
置换的复合 置换的 \(k\) 次幂 求循环节
\(\min\)/\(\max\)-plus 矩阵 最短路的 \(k\) 步转移 图论

81.2 Python 取舍:内置 pow 不可替代

CPython 的三参数 pow(a, b, m)纯 C 实现的模幂,对大指数还会自动切换到 5 位滑动窗口算法(比朴素二进制拆分少约 20% 的乘法)。

实测对比(Python 3.9,\(10^5\) 次调用,\(a \le 10^9\)\(b \sim 10^{18}\)\(m = 10^9+7\)):

写法 耗时 相对
pow(a, b, m)(内置) 0.37 s
手写迭代快速幂 0.82 s 2.2×
手写递归快速幂(直译 pow.cpp 1.13 s 3.0×

差距看起来「只有」2–3 倍,但请注意:\(T = 2\times10^5\) 组的模板题里, 这就是 0.4 秒和 1.2 秒的区别,加上 IO 与解析开销,足以决定 AC 还是 TLE

而真正的数量级差距在另一个地方——千万不要写 a ** b % m

# ❌ 灾难:先算出完整的大整数 a**b,再取模
(a ** b) % m

# ✅ 边乘边模,中间量始终 < m²
pow(a, b, m)

实测\(b \sim 10^5\),200 次调用):

写法 耗时
pow(a, b, m) < 0.001 s
(a ** b) % m 15.7 s

差了 5 个数量级。原因很简单:\(a^b\) 有约 \(b\log_{10}a \approx 9\times10^5\) 位, 光是把它算出来就要做几十万次大整数乘法,最后那次取模还是 \(O(d^2)\) 的。 \(b\) 再大一点直接爆内存。

这是 Python 大整数「不溢出」这个优点的阴暗面: C++ 写 a ** b % m 会立刻溢出得到垃圾值,你马上就会发现错了; Python 会老老实实地把它算出来,然后卡死。 参见 22-高精度与大整数

pow 的三个使用注意

情况 行为
pow(a, b, m)b >= 0 正常模幂,结果在 \([0, m)\)
pow(a, -1, m) 求逆元(3.8+),要求 \(\gcd(a,m)=1\),否则 ValueError
pow(a, -k, m) \(= (a^{-1})^k\),同样要求互质
pow(a, b, 0) ValueError: pow() 3rd argument cannot be 0
a 为负 自动先 a % m,结果仍非负

81.3 快速乘:一个 Python 用不上的模板

C++ 里当模数 \(m\) 达到 \(10^{18}\) 时,a * b % m 会溢出 long long\(10^{18} \times 10^{18} = 10^{36}\),远超 \(9.2\times10^{18}\))。 周尚彦的课件专门提了这个问题:

快速乘法:给定 \(a, b, m \le 10^{18}\),求 \(a \times b \bmod m\)

C++ 的解法有三种:__int128、「龟速乘」(把乘法拆成 \(O(\log b)\) 次加法, 和快速幂同一个套路)、以及基于浮点的 \(O(1)\) 技巧。

Python 完全不需要这些

a * b % m          # a, b, m 随便多大都对

因为 int 是任意精度的。\(10^{18}\) 量级的乘法在 CPython 里是 2 个 limb 乘 2 个 limb, 几乎和小整数一样快。

这是 Python 在数论题里最大的一处便宜: 凡是题面写着「模数 \(\le 10^{18}\)」「需要 __int128」「需要龟速乘」的题, Python 选手直接跳过整个模板。 Miller-Rabin、Pollard-Rho 在 C++ 里的一大半代码量都花在防溢出上, Python 版本因此短得多——见 83-整除分块与数论进阶

但要记住代价:当模数真的是 \(10^{18}\) 时,Python 的每次 a*b%m 也确实比 \(10^9\) 模数下慢一些(多一个 limb),而且 pow(a,b,m) 的常数会随模数位数上升。 实测 128 位模数下内置 pow 与手写快速幂的差距会从 2.2× 缩小到 1.3×—— 因为此时瓶颈变成了大整数乘法本身,而不是解释器开销。


81.4 模意义下的除法问题

李有为的课件说得最直白:

但是我们少了除法。仔细一想我们只说了整数能对应到 \(0 \ldots b-1\), 没说有理数能对应到 \(0 \ldots b-1\)

在模 \(m\) 的世界里,只有加、减、乘是天然封闭的。 除法必须重新定义:「除以 \(b\)\(:=\) 「乘以 \(b\) 的乘法逆元」

定义:若 \(b x \equiv 1 \pmod m\),称 \(x\)\(b\) 在模 \(m\) 意义下的乘法逆元, 记作 \(b^{-1}\)\(\operatorname{inv}(b)\)

rxz 课件里的例子:\(4\)\(3\) 在模 \(11\) 意义下的逆元(\(3\times4=12\equiv1\)); \(2\)\(7\) 在模 \(13\) 意义下的逆元(\(7\times2=14\equiv1\))。

逆元什么时候存在?

rxz 的课件把这个留成了课后习题(「什么时候找不到逆元?」)。答案由裴蜀定理给出:

定理\(b\) 在模 \(m\) 下有逆元 \(\iff \gcd(b, m) = 1\)。此时逆元在模 \(m\) 下唯一。

证明\(bx \equiv 1 \pmod m\) 等价于 \(bx + my = 1\) 有整数解。 由裴蜀定理,这等价于 \(\gcd(b,m) \mid 1\),即 \(\gcd(b,m)=1\)。 唯一性:若 \(bx \equiv bx' \equiv 1\),由消去律(\(\gcd(b,m)=1\))得 \(x \equiv x'\)\(\square\)

两个直接推论

  • 模数 \(P\)质数时,\(1, 2, \ldots, P-1\) 全都有逆元(只有 \(0\) 没有)—— 这就是 rxz 课件说的「为什么模质数」;
  • 模数不是质数时,只有与 \(m\) 互质的数才有逆元。\(m = 12\)\(2, 3, 4, 6, 8, 9, 10\) 全都没有逆元。

81.5 费马小定理

定理(Fermat)\(P\) 是质数且 \(\gcd(a, P) = 1\),则 \(a^{P-1} \equiv 1 \pmod P\)

胡渊鸣课件给的证明非常漂亮,抄录并补全:

小引理:给定模 \(P\) 意义下两两不同余的一组数 \(a_1, \ldots, a_n\), 对于任意 \(b\)\(P\) 互质,\(a_1b, a_2b, \ldots, a_nb\) 仍然两两不同余。 (由消去律:\(a_ib \equiv a_jb \Rightarrow a_i \equiv a_j\)。)

证明\(\{1, 2, \ldots, P-1\}\) 显然两两不同余。由于 \(\gcd(a,P)=1\), 根据小引理 \(\{a, 2a, \ldots, (P-1)a\}\) 也两两不同余, 且每个都不同余于 0(否则 \(P \mid ka\),而 \(\gcd(a,P)=1\) 推出 \(P \mid k\),与 \(k < P\) 矛盾)。 所以它在模 \(P\) 意义下一定是 \(\{1, 2, \ldots, P-1\}\) 的一个排列。 两边取乘积: $\(\prod_{k=1}^{P-1}(ka) \equiv \prod_{k=1}^{P-1} k \pmod P\)$ $\((P-1)!\, a^{P-1} \equiv (P-1)! \pmod P\)$ 因 \(\gcd((P-1)!, P) = 1\),两边消去 \((P-1)!\),定理得证。\(\square\)

推论(求逆元)\(a \cdot a^{P-2} = a^{P-1} \equiv 1\),所以

\[a^{-1} \equiv a^{P-2} \pmod P \qquad (P \text{ 为质数},\ P \nmid a)\]

费马小定理是欧拉定理的特例\(P\) 是质数时 \(\varphi(P) = P-1\)), 见 82-欧拉函数与欧拉降幂

费马小定理的逆命题不成立\(2^{340} \equiv 1 \pmod{341}\), 但 \(341 = 11 \times 31\) 是合数。这类数叫伪素数(Carmichael 数更强)。 这正是 Miller-Rabin 要在费马测试基础上加强的原因,见 83-整除分块与数论进阶


81.6 求逆元的四种方法

方法一:费马小定理 + 快速幂(模数为质数)

# [片段] 模板:质模数下的逆元
P = 10 ** 9 + 7
inv_a = pow(a, P - 2, P)         # 要求 P 是质数且 P 不整除 a

\(O(\log P)\),一行。质模数题的标准写法。

方法二:扩展欧几里得(模数任意)

模数不是质数时费马小定理彻底失效(\(a^{m-2}\) 不再是逆元)。 解 \(ax + my = 1\)

# [片段] 模板:扩展欧几里得求逆元(任意模数)
def exgcd(a, b):
    """返回 (g, x, y) 满足 a*x + b*y = g = gcd(a,b)。迭代版。"""
    old_r, r = a, b
    old_s, s = 1, 0
    while r:
        q = old_r // r
        old_r, r = r, old_r - q * r
        old_s, s = s, old_s - q * s
    return old_r, old_s, (old_r - old_s * a) // b if b else 0


def inv_exgcd(a, m):
    """a 在模 m 下的逆元;不存在返回 -1。"""
    old_r, r = a, m
    old_s, s = 1, 0
    while r:
        q = old_r // r
        old_r, r = r, old_r - q * r
        old_s, s = s, old_s - q * s
    if old_r != 1:                # gcd(a, m) != 1 -> 逆元不存在
        return -1
    return old_s % m              # ★ 可能是负数,Python 的 % 自动拉回 [0, m)

Python 捷径

pow(a, -1, m)                     # 3.8+,内部就是扩展欧几里得

pow(a, -1, m) 的边界行为一定要清楚: - \(\gcd(a, m) = 1\):正常返回 \([0, m)\) 内的逆元,不要求 \(m\) 是质数; - \(\gcd(a, m) > 1\):抛 ValueError: base is not invertible for the given modulus; - \(m = 1\):返回 0(模 1 下一切同余)。

所以在非质模数题里,必须包 try/except,否则一组不互质的数据就 RE:

try:
    v = pow(a, m - 1 - 1, m)   # ← 错的!非质模数不能用费马小定理
except ...:
    ...
正确写法:
try:
    v = pow(a, -1, m)
except ValueError:
    v = -1                      # 逆元不存在

方法三:线性递推求 \(1 \ldots n\) 的全部逆元(\(O(n)\)

胡渊鸣称之为「比较不为人知的一种可以在 \(O(n)\) 时间内完成此任务的莫名其妙的算法」, 邹雨恒的课件给了同样的推导。S3 day9 的 逆元.cpp 就是这段代码。

推导:设 \(P = \lfloor P/i \rfloor \cdot i + (P \bmod i)\)。两边模 \(P\): $\(0 \equiv \left\lfloor \frac{P}{i} \right\rfloor i + (P \bmod i) \pmod P\)$ $\((P \bmod i) \equiv -\left\lfloor \frac{P}{i} \right\rfloor i \pmod P\)$ 两边同乘 \(i^{-1} (P \bmod i)^{-1}\): $\(i^{-1} \equiv -\left\lfloor \frac{P}{i} \right\rfloor (P \bmod i)^{-1} \pmod P\)$ 因为 \(P \bmod i < i\),所以从小到大计算时 \((P \bmod i)^{-1}\) 已经算出来了。\(\square\)

# [片段] 模板:O(n) 递推求 1..n 的全部逆元(P 为质数且 P > n)
def inv_table(n, P):
    """inv[i] 是 i 在模 P 下的逆元,i = 1..n。"""
    inv = [0] * (n + 1)
    inv[1] = 1
    for i in range(2, n + 1):
        inv[i] = (P - P // i) * inv[P % i] % P     # ★ 用 P - P//i 代替 -P//i,避免负数
    return inv

对照 逆元.cpp 的原始 C++(\(p = 11\) 的演示):

inv[1]=1;
for(i=2;i<=10;i++)
    inv[i]=(p-p/i)*inv[p%i]%p;

使用前提\(P\) 必须是质数,且 \(n < P\)(否则会用到 \(P\) 自己的逆元,不存在)。

Python 的现实评估:这是一个 \(n\) 次的纯 Python 循环\(n = 10^6\) 约 0.35 秒,\(n = 10^7\) 就要 3.5 秒——比调 \(10^6\)pow(i, P-2, P) (C 层,约 3.4 秒)只快 10 倍,远不如在 C++ 里那么划算。 而且大多数题真正需要的是阶乘逆元,用下面的「倒推法」只要一次 pow

方法四:阶乘逆元倒推(组合数题的标配)

利用 \(\frac{1}{(i-1)!} = \frac{i}{i!}\)

\[\operatorname{invfact}[i-1] = \operatorname{invfact}[i] \times i \pmod P\]

只要一次模幂就能得到全部阶乘逆元。

# [片段] 模板:阶乘表 + 阶乘逆元倒推(组合数取模的标准前置)
def build_fact(n, P):
    """返回 (fact, inv_fact),下标 0..n。只做一次 pow。O(n)。"""
    fact = [1] * (n + 1)
    for i in range(2, n + 1):
        fact[i] = fact[i - 1] * i % P
    inv_fact = [1] * (n + 1)
    inv_fact[n] = pow(fact[n], P - 2, P)        # ★ 唯一的一次模幂
    for i in range(n, 0, -1):
        inv_fact[i - 1] = inv_fact[i] * i % P   # 1/(i-1)! = i * (1/i!)
    return fact, inv_fact

顺带地,\(\operatorname{inv}[i] = \operatorname{invfact}[i] \times \operatorname{fact}[i-1]\), 所以这张表也能 \(O(1)\) 给出单个数的逆元。

方法五:批量求逆(任意一组数,只用一次模幂)

给定 \(a_1, \ldots, a_n\)(都可逆),要求全部逆元。做前缀积,对最后一个求逆,再倒推:

# [片段] 模板:批量求逆(Montgomery trick),n 个数只做一次 pow
def batch_inverse(a, P):
    """返回列表 inv,inv[i] = a[i]^(-1) mod P。要求每个 a[i] 都与 P 互质。"""
    n = len(a)
    pre = [1] * (n + 1)                  # pre[i] = a[0]*...*a[i-1]
    for i in range(n):
        pre[i + 1] = pre[i] * a[i] % P
    cur = pow(pre[n], P - 2, P)          # ★ 唯一的一次模幂
    inv = [0] * n
    for i in range(n - 1, -1, -1):
        inv[i] = cur * pre[i] % P        # 1/a[i] = (1/(a0..ai)) * (a0..a_{i-1})
        cur = cur * a[i] % P             # 退化到 1/(a0..a_{i-1})
    return inv

四种方法的选择表

方法 复杂度 前提 什么时候用
pow(a, P-2, P) \(O(\log P)\) \(P\) 质数 求单个逆元的默认写法
pow(a, -1, m) \(O(\log m)\) \(\gcd(a,m)=1\) 模数非质;要 try/except
手写 exgcd \(O(\log m)\) 同上 需要区分无解或要通解时
线性递推 inv[i] \(O(n)\) \(P\) 质数,\(n<P\) \(1\ldots n\) 全部逆元;Python 下 \(n \le 10^6\)
阶乘逆元倒推 \(O(n)\) + 1 次 pow \(P\) 质数,\(P>n\) 组合数题必用84 章
批量求逆 \(O(n)\) + 1 次 pow 每个都可逆 一组任意的数要全部求逆

81.7 分数取模

「求 \(\frac{a}{b} \bmod P\)」这个说法要正确理解:它不是先算浮点 \(a/b\) 再取模, 而是求那个唯一的 \(x \in [0, P)\) 满足 \(bx \equiv a \pmod P\)

# [片段] 模板:分数取模
P = 10 ** 9 + 7
x = a % P * pow(b, P - 2, P) % P      # P 为质数
x = a % P * pow(b, -1, P) % P         # 等价写法,且不要求 P 是质数

Python 的一个白捡便宜\(a\) 是负数时,C++ 要写 ((a % P) + P) % P, Python 的 % 直接给出 \([0,P)\) 内的结果。BISHI65 的 \(a\) 下界是 \(-10^9\), Python 选手完全不用管这件事。

含分数的整个表达式:先把所有分子乘起来、所有分母乘起来(各自随时取模), 最后只求一次逆元。不要每一项都求一次逆元。

\[\frac{a_1}{b_1}\cdot\frac{a_2}{b_2}\cdots\frac{a_n}{b_n} \equiv \left(\prod a_i\right) \cdot \left(\prod b_i\right)^{-1} \pmod P\]

81.8 例题

BISHI64 【模板】快速幂Ⅰ ‖ 模小整数(中等)

\(T \le 2\times10^5\) 组,每组给 \(0 \le a, b \le 10^9\)(且 \(a+b>0\))、\(1 \le p \le 10^9\), 求 \(a^b \bmod p\)。时限 C/C++ 5 秒、其他语言 10 秒。 题面见 BISHI64 原题(牛客)

这题在 Python 里的正确答案就是「不要写快速幂」:直接调 pow

import sys


def main():
    data = sys.stdin.buffer.read().split()
    t = int(data[0])
    out = []
    ap = out.append
    idx = 1
    for _ in range(t):
        a = int(data[idx]); b = int(data[idx + 1]); p = int(data[idx + 2])
        idx += 3
        ap(str(pow(a, b, p)))        # 内置三参数 pow = C 实现的快速幂
    sys.stdout.write("\n".join(out) + "\n")


main()

三个坑

  1. \(p\) 可以等于 1!样例第一行 1 0 1 的答案是 0,不是 1。 手写快速幂如果把 res 初始化成 1 而忘了最后 % p,这里就错。 内置 pow(1, 0, 1) 返回 0,天然正确;
  2. \(a\) 可以为 0(题目只保证 \(a+b>0\),排除了 \(0^0\))。pow(0, b, p)\(b\ge1\) 返回 0;
  3. \(T = 2\times10^5\),逐行 input() 会被 IO 拖死,必须 sys.stdin.buffer.read().split() + 一次性 join 输出。

瓶颈分析:本题真正的耗时不在模幂(\(2\times10^5\) 次 C 层 pow 约 0.7 秒), 而在 \(6\times10^5\)int() 解析。所以 IO 优化比算法优化更重要—— 这是 Python 模板题的普遍现象,见 20-输入输出处理

题解见 solutions/BISHI64.py

BISHI65 【模板】分数取模(中等)

\(t \le 10^4\) 组,每组给 \(-10^9 \le a \le 10^9\)\(1 \le b \le 10^9\), 求 \(\frac{a}{b} \bmod P\)\(P = 10^9+7\)(质数),答案保证在 \([0, P-1]\)。 题面见 BISHI65 原题(牛客)

\(P\) 是质数且 \(1 \le b \le 10^9 < P\),所以 \(P \nmid b\),逆元必然存在。 直接费马小定理:

import sys

P = 1000000007


def main():
    data = sys.stdin.buffer.read().split()
    t = int(data[0])
    out = []
    ap = out.append
    for i in range(t):
        a = int(data[1 + 2 * i]); b = int(data[2 + 2 * i])
        # 等价写法:inv = pow(b, -1, P)(3.8+,走扩展欧几里得,不要求 P 是质数)
        ap(str(a * pow(b, P - 2, P) % P))     # Python 的 % 天然把负数拉回 [0,P)
    sys.stdout.write("\n".join(out) + "\n")


main()

三个坑

  1. \(a\) 可能是负数。样例 2 的 -114514 1919810 答案是 101224601。 Python 的 % 永远返回非负,一个字符都不用改;C++ 必须补 ((x%P)+P)%P
  2. 别真的去算浮点 a / b 再取模——那是完全不同的东西;
  3. 不要每组重新写快速幂\(10^4\) 次 C 层 pow 只要 0.03 秒,瓶颈仍然是 IO。

对照检验\(\frac12 \bmod (10^9+7)\)\(2^{-1} = \frac{P+1}{2} = 500000004\),样例第一行正是它。 一个好用的口诀:\(P\)\(\frac12\) 就是 \(\frac{P+1}{2}\)\(P\) 为奇质数)。

题解见 solutions/BISHI65.py

BISHI74 【模板】非质模数下的乘法逆元(中等)

\(T \le 10^4\) 组,每组给 \(1 \le a < m \le 10^9\),求 \(a^{-1} \bmod m\)数据不保证 \(m\) 为质数! 题面见 BISHI74 原题(牛客)

题面那句加粗的提示就是全部考点:费马小定理在这里彻底失效\(a^{m-2}\) 一般不是逆元——比如 \(m = 12, a = 5\)\(5^{10} \bmod 12 = 1\) 纯属巧合, 换成 \(m = 15, a = 2\)\(2^{13} \bmod 15 = 8\),而真正的逆元是 \(8\)…… 这种「有时对有时错」比全错更危险,绝不能碰运气。

正解是扩展欧几里得:解 \(ax + my = \gcd(a,m)\)\(\gcd=1\)\(x \bmod m\) 就是逆元。

import sys


def inv(a, m):
    """扩展欧几里得求 a 在模 m 下的逆元;不存在返回 -1。迭代版,无递归开销。"""
    # 维护 old_r = old_s * a + (...) * m,对 (r, s) 做辗转相除
    old_r, r = a, m
    old_s, s = 1, 0
    while r:
        q = old_r // r
        old_r, r = r, old_r - q * r
        old_s, s = s, old_s - q * s
    if old_r != 1:            # gcd(a, m) != 1 -> 逆元不存在
        return -1
    return old_s % m          # 可能是负数,拉回 [0, m)


def main():
    data = sys.stdin.buffer.read().split()
    t = int(data[0])
    out = []
    for i in range(t):
        a = int(data[1 + 2 * i]); m = int(data[2 + 2 * i])
        # 等价一行写法(Python 3.8+):
        #   try: v = pow(a, -1, m)
        #   except ValueError: v = -1
        out.append(str(inv(a, m)))
    sys.stdout.write("\n".join(out) + "\n")


main()

四个坑

  1. 逆元不存在时输出什么,题面没说。输出描述只写「输出一行一个正整数」, 从措辞看数据应当保证 \(\gcd(a,m)=1\);本题解在无解时输出 \(-1\)(竞赛最常见约定), 不影响有解数据的正确性。遇到题面歧义时,选一个不影响合法数据的兜底值
  2. exgcd 算出的 \(x\) 可能是负数,必须 % m 拉回 \([0, m)\)
  3. 别写递归 exgcd——\(T=10^4\)\(4.5\times10^5\) 次函数调用开销明显;
  4. \(m = 1\) 时按惯例逆元记 0,但题目给了 \(a < m\) 所以 \(m \ge 2\),不会出现。

实战里该怎么写? 直接用内置:

# [片段]
try:
    v = pow(a, -1, m)
except ValueError:
    v = -1
pow(a, -1, m) 内部就是扩展欧几里得,C 实现,比上面的 Python 循环快 3–5 倍。 上面手写一遍是为了讲清原理——模板题的意义在于让你知道内置函数在做什么

题解见 solutions/BISHI74.py


81.9 本章速查

公式

名称 内容
快速幂 \(a^b = \prod_{c_i=1} a^{2^i}\)\(O(\log b)\)
费马小定理 \(P\) 质数、\(\gcd(a,P)=1\) \(\Rightarrow\) \(a^{P-1}\equiv1 \pmod P\)
费马求逆 \(a^{-1} \equiv a^{P-2} \pmod P\)
逆元存在条件 \(\gcd(a,m)=1\)(裴蜀定理)
线性递推逆元 \(\operatorname{inv}[i] = (P - \lfloor P/i\rfloor)\cdot \operatorname{inv}[P \bmod i] \bmod P\)
阶乘逆元倒推 \(\operatorname{invfact}[i-1] = \operatorname{invfact}[i]\cdot i\)
分数取模 \(\frac ab \equiv a \cdot b^{-1} \pmod P\)
\(\frac12 \bmod P\) \(\frac{P+1}{2}\)\(P\) 为奇质数)

Python 取舍

场景 做法 理由
模幂 pow(a, b, m) C 实现 + 滑动窗口,比手写快 2–3 倍
绝对不要 (a ** b) % m 大整数膨胀,实测慢 \(10^5\)
质模数求逆 pow(a, P - 2, P) 一行
任意模数求逆 pow(a, -1, m) + try/except ValueError 内部是 exgcd,不要求质模数
需要判无解 / 求通解 手写迭代 exgcd pow 只抛异常
\(1..n\) 全部逆元 线性递推(\(n\le10^6\) 纯 Python 循环,\(n\) 大就不划算
阶乘逆元 倒推,只做一次 pow 组合数题标配
一组任意数求逆 批量求逆(前缀积) 同样只做一次 pow
快速乘 / __int128 不需要 Python int 任意精度
负数取模 直接 % 结果天然非负
\(m = 1\) 内置 pow 自动返回 0 手写快速幂要补最后一次 % m

陷阱清单

陷阱 后果
模数可能为 1 手写快速幂返回 1 而不是 0
非质模数用费马小定理 偶尔碰巧对,更难查
pow(a,-1,m)try \(\gcd>1\)ValueError → RE
每项都求一次逆元 白白多做 \(n\) 次模幂,应先乘起来最后求一次
a ** b % m 大整数爆炸
递归 exgcd 大量询问时函数调用开销显著