分享丨模运算的世界:当加减乘除遇上取模(模运算恒等式/费马小定理/组合数)
59312
2024.05.14
2025.11.21
发布于 浙江

模运算规则 逆元 费马小定理 快速幂 灵茶山艾府

前言

某些题目,由于要计算的答案非常大(超出 64 位整数的范围),会要求把答案对 取模。如果没有处理得当的话,会 WA(错误)或者 TLE(超时)。

例如计算一堆数字的乘积,如果没有及时取模,乘法会溢出(例如计算结果超出 C++ 中 long long 的最大值),从而得到和预期不符的答案。对于 Python 来说,虽然没有溢出的问题,但大整数(big integer)之间的运算并不是 的,可能会导致 TLE。

如何正确地取模呢?

加法和乘法的取模

如果让你计算 个位数,你会如何计算?

由于只有个位数会影响到乘积的个位数,那么 的个位数 就是答案。

对于 的个位数,同理, 的个位数 就是答案。

你能把这个结论抽象成数学等式吗?

一般涉及到取模的题目,会用到如下两个恒等式,其中 表示取模运算(modulo),即编程语言中的 %。上面计算的是 的情况。

证明:根据带余除法,任意整数 都可以表示为 ,其中整数 除以 的商(quotient),整数 除以 的余数(remainder),即

设整数

第一个恒等式:

第二个恒等式:

根据这两个恒等式,我们可以在计算过程中(例如循环),对加法和乘法的结果取模,而不是在循环结束后再取模。

:如果涉及到幂运算,指数是不能随意取模的。如果指数在 64 位整数的范围内,可以用快速幂计算,原理见【图解】一张图秒懂快速幂;如果指数超出 64 位整数的范围,见 欧拉降幂

如果计算过程中有减法,可能会产生负数,处理不当也会导致 WA。如何正确处理这种情况呢?

同余

首先引入同余(congruence modulo)的概念。

两个整数 ,如果 (也就是 的倍数),则称 关于模 同余,记作

上式也称作 的同余式,简称同余式

例如

特别地,有

同余式的移项

同余式中的加减法可以移项。

例如

可以移项得到

证明 意味着 的倍数,即 的倍数,所以

推论:在同余式两边加上(减去)同一个数,同余式仍然成立。

证明:设 ,左边加上 ,得到 ,移项得

特别地,在同余式

的两边加上 ,得

例如在无符号 位整数中, 溢出得到 ,用数学语言描述,就是

负数和减法的取模

根据同余的定义,我们有

怎么从 得到

我们可以 。也可以 ,这样只需加一次

一般地,如果 ,则 相当于

也就是用「模 」,把 「调整」为非负数。

为避免判断 ,更一般的写法是

这样无论 是正是负还是零,运算结果都会落在区间 中。

对于减法来说,当 时,前文中的加法恒等式可以调整为

注 1:代码实现时,在计算中产生负数也可以,在最后用 调整就行。

注 2:Python 用户可以忽略,只要 是正整数,取模运算的结果就一定是非负整数。

以上是关于加法、减法和乘法的模运算规则,那么除法呢?

除法的取模

如果要计算 ,可以像加法或乘法那样,写成 吗?这不行, 甚至不是一个整数。

先说结论,如果 是一个质数, 的倍数且 互质( 不是 的倍数),那么有

上式中 可以是很大的数,例如

由于 是一个质数,所以上式可用于要求对 取模的题目。如果推导出了包含除法的式子,可以用上式转换成乘法,并用快速幂计算

下面是证明。

引理 1:当 是质数且 时,有

其中

证明:注意当 时, 的分母是不包含 的。由于分子包含 是整数,所以 可以被 整除,即

:如果 不是质数,分母可能被 整除,上面的结论不一定成立。例如 的情况,,不是 的倍数。

引理 2:对于任意整数 和任意质数 ,有

证明:根据二项式定理,有

根据引理 1,除了第一项和最后一项以外,其余项都是 的倍数,于是

定理(费马小定理):对于任意整数 和任意质数 ,有

证明:当 时, 成立。

假设 时原命题成立,即

根据引理 2,我们有

根据归纳假设,得

即当 时,原命题成立。

根据数学归纳法,原命题对于 成立。对于 的情况同理。

变形得 ,如果 不是 的倍数,那么 必须是 的倍数(注意 是质数),即 ,移项得

两边同时乘以 的倍数),得

注:除以 相当于乘以 逆元 。在概率期望等题目中,会遇到 不是 的倍数的情况,这些题目通常会规定计算 ,计算方法和上式一样。

总结

代码实现时,上面的加减乘除通常是这样写的:

MOD = 1_000_000_007

// 加
(a + b) % MOD

// 减,b 在 [0,MOD-1] 中
(a - b + MOD) % MOD

// 把任意整数 a 取模到 [0,MOD-1] 中,无论 a 是正是负
(a % MOD + MOD) % MOD

// 乘(注意使用 64 位整数)
a * b % MOD

// 多个数相乘,要步步取模,防止溢出
a * b % MOD * c % MOD

// 除(MOD 是质数且 b 不是 MOD 的倍数)
a * qpow(b, MOD - 2, MOD) % MOD

其中 快速幂,具体请看【图解】一张图秒懂快速幂

注:Python 内置快速幂函数 pow(x, y, m) 用于计算 。特别地,除法也可以写成 a * pow(b, -1, MOD) % MOD

总之,如果发现解答错误,可以检查下代码,看看是不是哪里漏掉取模了。

附:组合数的计算

关于组合数,我们需要预处理阶乘及其逆元,然后利用公式

计算。

对于阶乘 ,可以用

递推计算。

对于阶乘的倒数 ,可以先计算 的逆元(其中 的最大值),然后用

倒着递推计算。

模板代码如下:

Python3
Java
C++
Go
MOD = 1_000_000_007
MX = 100_001  # 根据题目数据范围修改

fac = [0] * MX  # fac[i] = i!
fac[0] = 1
for i in range(1, MX):
    fac[i] = fac[i - 1] * i % MOD

inv_f = [0] * MX  # inv_f[i] = i!^-1
inv_f[-1] = pow(fac[-1], -1, MOD)
for i in range(MX - 1, 0, -1):
    inv_f[i - 1] = inv_f[i] * i % MOD

# 从 n 个数中选 m 个数的方案数
def comb(n: int, m: int) -> int:
    return fac[n] * inv_f[m] * inv_f[n - m] % MOD if 0 <= m <= n else 0

class Solution:
    def solve(self, nums: List[int]) -> int:
        # 预处理的逻辑写在 class 外面,这样只会初始化一次

如果模数不是质数呢?见 3463. 判断操作后字符串中的数字是否相等 II

取模练习

快速幂练习

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
  7. 动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

评论 (100)