
某些题目,由于要计算的答案非常大(超出 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。
总之,如果发现解答错误,可以检查下代码,看看是不是哪里漏掉取模了。
关于组合数,我们需要预处理阶乘及其逆元,然后利用公式
计算。
对于阶乘 ,可以用
递推计算。
对于阶乘的倒数 ,可以先计算 的逆元(其中 是 的最大值),然后用
倒着递推计算。
模板代码如下:
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。
欢迎关注 B站@灵茶山艾府