
前言
某些题目,由于要计算的答案非常大(超出 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] 中,通过 +MOD 保证结果非负
(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。
总之,如果发现解答错误,可以检查下代码,看看是不是哪里漏掉取模了。
附:组合数的计算
关于组合数,我们需要预处理阶乘及其逆元,然后利用公式
计算。
对于阶乘 ,可以用
递推计算。
对于阶乘的倒数 ,可以先计算 的逆元(其中 是 的最大值),然后用
倒着递推计算。
模板代码如下:
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。
取模练习
- 3379. 转换数组 做到
- 1018. 可被 5 整除的二进制前缀 1376
快速幂练习
- 2961. 双模幂运算 1451
- 2550. 猴子碰撞的方法数 1663
- 372. 超级次方
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
- 动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
欢迎关注 B站@灵茶山艾府


