本文中模运算仅对 \mathbb{Z} 中的数定义。
前言
在组合数学中,常常会遇到特别大的答案,这些答案极有可能超出 long long
甚至 int128
的范围。为判断答案的正确性,题目会要求将答案取模。因模运算的性质,程序可以在运算过程中对阶段答案不断取模,保证运算结果在任何时候不超出数据类型的范围。
基本概念
模运算英文名为\text{Modular Arithmetic},“模”是音译的,模运算的实际是求余运算,是一种二元运算,符号为\bmod,a模b记为"a \bmod b",运算结果为a除以b的余数。
形如a \equiv b \pmod m的式子称为同余式,表示a \bmod m = b \bmod m,也可理解为a与b的差为m的倍数。同余式满足自反律(a \equiv a)、对称律(a \equiv b \Rightarrow b \equiv a)、传递律(a \equiv b \equiv c \Rightarrow a \equiv c)。
基本运算性质
若a\equiv b{\pmod {m}},c\equiv d{\pmod {m}},则有
\text{(i.1.3)}可推导出
由这些基本运算性质可知,若题目求答案对m取模的值,在每一步加减乘运算之后对结果取模,最终的结果等于直接对答案取模。这样就能保证运算过程中的数和最终结果都不超出数据类型的范围。
注意:在模数下进行减法运算时,可能遇到结果为负数的情况,C++规定模运算的结果的正负性和被模数的正负性相同,所以可能导致最终答案为负,通常的做法是在减法后加上模数再模上模数。
除法和逆元
上述运算性质中并没有关于除法的性质。
事实上,ad \equiv bd \Rightarrow a \equiv b \pmod{m}并不一定成立。小学数学告诉我们,除以一个数等于乘上这个数的倒数。但d \in \mathbb{Z},所以\frac{1}{d} \in [-1, 1],而\text{(i.1.3)}是在a,b,c,d \in \mathbb{Z}的情况下推导出来的,所以不能使用\text{(i.1.3)}的性质。
如果我们能在\mathbb{Z}中找到一个可以代替\frac{1}{d}的数,ad \equiv bd \Rightarrow a \equiv b \pmod{m}自然就成立了。
不妨设设该数为d',如果需要满足d'd=1,显然找不到代替\frac{1}{d}的数;但根据\text{(i.1.3)},我们只需要满足d'd \equiv 1 \pmod{m}就可以了。
d'd \equiv 1 \pmod{m}可化为d'd+m'm=1(其中m' \in \mathbb{Z},m'和d'均为未知数)。
裴蜀定理( \text{Bézout's Lemma} ): 对于a,b \in \mathbb{Z^+}且a,b不全为0,方程ax+by=c有解的充要条件为gcd(a,b)|c。
对于d'd+m'm=1,因为此时c=1,所以该方程有整数解当且仅当gcd(d, m)=1,即d \bot m。
所以,若d \bot m,
现在我们考虑在程序运算过程中出现除法如何处理。
设res为该次运算不取模的结果,a为未取模的被除数,a'为已取模的被除数,d为除数,则有\frac{a}{d}=res \Rightarrow a=res \cdot d,a' \equiv res \cdot d \pmod{m}。
接下来考虑已知a'和d,求res \bmod m。
如果d \bot m,且d|a',根据我们推导出的\text{(i.2)},res \equiv \frac{a'}{d} \pmod{m}。
但大多数情况下d|a'并不成立,这时我们像前面推导\text{(i.2)}时一样考虑将同余式两边同乘代替\frac{1}{d}的数d',就得到了a' \cdot d' \equiv res \pmod{m}。
我们称d'为d的逆元。因为它的作用很像d的倒数(我们其实就是拿它在\text{mod } m下代替倒数的),即d^{-1},所以我们一般也把它记作d^{-1}。
如果一个数d存在逆元d^{-1},则d^{-1} 不唯一(裴蜀定理还有一条内容:若二元不定整数方程ax+by=c有整数解,则该方程有无数多组整数解),一般我们求的d^{-1}是一个特解。
前面已经证明d^{-1}d+m'm=1有解当且仅当d \bot m,所以d^{-1} 仅在d \bot m 时存在。通常情况下,题目给的模数m都是比较大的质数,只要d<m,那么d \bot m。即便m不是质数,数据也会保证d \bot m,或者不会出现除法运算。
逆元求法
扩展欧几里得法 EXGCD
根据我们前面的推导过程,d^{-1}是d^{-1}d \equiv 1 \pmod{m},即d^{-1}d+m'm=1(m \in \mathbb{Z})的解。所以我们直接解这个方程就可以了。
裴蜀定理用于判断二元不定整数方程是否有整数解,而它实际也给了我们一个解二元不定整数方程的启示。因为二元不定整数方程ax+by=c有解的充要条件为gcd(a,b)|c,对于任意一个这样的方程,我们只要求得一组ax+by=gcd(a,b)的解,乘上\frac{c}{gcd(a,b)}就得到原方程的一组特解了(求出一组特解,通解也就出来了)。对于ax+by=gcd(a,b),其实我们在用欧几里得法求gcd(a,b)的时候就可以一并求出了。这个用欧几里得法求二元不定整数方程的整数解的方法称为扩展欧几里得法。
回到原问题,求d^{-1}d+m'm=1(m \in \mathbb{Z})的解。
该方程有解时gcd(d,m)=1,直接求d^{-1}d+m'm=gcd(d,m)就行了,不需要乘倍数了。
欧几里得算法通过下面的递归式求解:
接下来我们考虑在求gcd(m,n)时如何一并求出满足m'm+n'n=gcd(m,n)的一组整数m',n'。
- 递归到gcd(0,n)的情况,直接令m'=0,n'=1
- 递归到gcd(m,n)的情况,
此时要继续递归gcd(n \bmod m, m),我们设r为n \bmod m,则下一层递归为gcd(r, m),gcd(r,m)也会返回一组n',m',为方便区分,我们把它们分别记为\overline{r},\overline{m}。因为是递归的,返回的\overline{r},\overline{m}是\overline{r}r+\overline{m}m=gcd(r,m)的一组解。我们现在要用\overline{r},\overline{m}表示当前这一层递归m',n'。因为gcd(r,m)=gcd(m,n),而\overline{r}r+\overline{m}m=gcd(r,m),m'm+n'n=gcd(m,n),可建立等式\overline{r}r+\overline{m}m=m'm+n'n。前面我们令r=n \bmod m,现在把它化成底函数的形式,r=n-\lfloor n/m \rfloor m,再代入等式,得到\overline{r}(n-\lfloor n/m \rfloor m)+\overline{m}m=m'm+n'n,再整理得到(\overline{m}-\lfloor n/m \rfloor \overline{r})m+\overline{r}n=m'm+n'n,于是\begin{cases}m'=\overline{m}-\lfloor n/m \rfloor \overline{r} \\ n'=\overline{r}\end{cases},这就是转移方程。
现在我们可以用这个方法解d^{-1}d+m'm=gcd(d,m)从而求得d^{-1}了。
代码如下:
void exgcd(int a, int b, int& x, int& y) {
if (b == 0) {
x = 1, y = 0;
return;
}
exgcd(b, a % b, y, x);
y -= a / b * x;
}
(代码来自OI Wiki)
T(n)=O(\log n)(和欧几里得法求gcd相同)
快速幂法
费马小定理( \text{Fermat's Little Theorem} ) :若p为质数,\gcd(a, p) = 1,则a^{p - 1} \equiv 1 \pmod{p}。
若模数m为质数,且d \bot m,就有d^{m-1} \equiv 1 \pmod{m}。
联立方程\begin{cases}d^{m-1} \equiv 1 \\ d^{-1}d \equiv 1 \end{cases}\pmod{m},得d^{-1}d \equiv d^{m-1} \pmod{m},从而d^{-1} \equiv d^{m-2},用快速幂求就可以了。
注意:只有在模数m为质数的情况下才能用快速幂求逆元。
代码就是快速幂的代码:
int qpow(long long a, int b) {
int ans = 1;
a = (a % p + p) % p;
for (; b; b >>= 1) {
if (b & 1) ans = (a * ans) % p;
a = (a * a) % p;
}
return ans;
}
int GetInv(long long d, int m){
return qpow(d,m-2);
}
T(n)=O(\log n)(快速幂的时间复杂度)
线性求逆元
该方法可在O(n)的时间复杂度下求出d \in \mathbb{Z} \cap [1,n]对应的d^{-1}。
线性算法通常是递推。
警告:因为我不知道OI Wiki上那个方法是怎么想到的,所以以下求递推式的过程是我瞎编的,谨慎阅读!
设f(x)=x^{-1},有f(1) \equiv 1 \pmod{m},因为\forall m \in \mathbb{Z},1 \times 1 \equiv 1 \pmod{m},这是递推的基础。
这个转移方程不好求。我们要设法找到f(i)与f(j)(j<i)的关系。显然f(i)的值与i,j,f(j),m有关。 因为j<i,在计算f(i)之前f(j)就已经被计算出来,所以只要确定j就行了。我们设f(i) \equiv R(i,j,m) \pmod{m}(R是一个我瞎编的NB的函数,假设我们已经确定j,它将i,j,f(j),m以某种我们猜不到的方式进行运算,得到一个关于模m与f(i)同余的结果),那么f(i)-R(i,j,m) \equiv 0 \pmod{m},f(i)-R(i,j,m)一定为m的倍数。我们尝试将m以某种NB的方式拆成一堆与f(i),i,j,f(j),m有关的东西的和。首先我们现在只知道i,所以我们先尝试强行把m拆成和i有关的东西,我们大胆地瞎猜合理地猜想m可分解为ki+b这种形式,我们现在只能确定i,所以k,b也应该与i有关,又因为我们要求的东西与同余有关,所以我们大胆地把m拆成\lfloor m/i \rfloor i + m \bmod i,则\begin{cases}k=\lfloor m/i \rfloor \\ b=m \bmod i\end{cases},这样保证了b<i,所以f(b)也是知道的,后面可以用。显然ki+b \equiv 0 \pmod{m},我们尝试让i^{-1}出现,将同余式两边同乘i^{-1},则k+bi^{-1} \equiv 0 \pmod{m},然后发现i^{-1}还有b这个系数,就再乘上b^{-1}把b消掉,b^{-1}就是f(b),是已经知道的,可以放心用,然后就有kf(b)+i^{-1} \equiv 0 \pmod{m},其中k,f(b)都是知道的,把\begin{cases}k=\lfloor m/i \rfloor \\ b=m \bmod i\end{cases}代入,再把kf(b)这一项移到右边,得到i^{-1} \equiv -\lfloor m/i \rfloor f(m \bmod i) \pmod{m},即f(i) \equiv -\lfloor m/i \rfloor f(m \bmod i) \pmod{m}。是的,就这样神奇地出来了。为什么?因为我照着OI Wiki抄的我们的猜想很合理。
最终递推式如下:
代码如下:
void GetInv(){
inv[1] = 1;
for (int i = 2; i <= n; ++i) {
inv[i] = (long long)(p - p / i) * inv[p % i] % p;
}
}
Reference 参考
- 《具体数学》
- OI Wiki
- 维基百科