取模看起来只是一个很普通的运算,但真正写进代码以后,负数、乘法溢出和模意义下的除法都很容易出错。特别是看到分式时,不能直接把普通除法搬进模运算,而是要先判断分母有没有逆元。

这篇文章从模运算与同余开始,接着介绍扩展欧几里得、线性同余方程、乘法逆元和欧拉定理,最后通过七道例题把这些内容连起来。

模运算与同余

取模的定义

对于整数$a$和正整数$m$,一定存在唯一的整数$q,r$,满足:

$$
a=qm+r,\qquad 0\le r<m
$$

其中$q$是商,$r$是$a$除以$m$得到的余数,记作:

$$
r=a\bmod m
$$

数学中的余数始终位于$[0,m-1]$。但C++中的 % 更接近“取余”,结果与被除数同号,因此:

1
cout << -3 % 5; // -3

而数学意义下的$-3\bmod5$应当等于$2$。如果需要把结果规范到$[0,m-1]$,先取模,再处理负数即可:

1
2
3
i64 x = -3, mod = 5;
x %= mod;
if (x < 0) x += mod;

例如:

$$
-3\equiv2\pmod5
$$

同余式:符号$\equiv$表示同余。例如$x\equiv y\pmod p$表示$x$与$y$除以$p$所得的余数相同;上式中,$-3$与$2$除以$5$所得的余数都是$2$。

处理以后$x=2$。

模运算的基本性质

设$m>0$,加法、减法和乘法都可以在运算过程中取模:

$$
(a+b)\bmod m=((a\bmod m)+(b\bmod m))\bmod m
$$

$$
(a-b)\bmod m=((a\bmod m)-(b\bmod m))\bmod m
$$

$$
(ab)\bmod m=((a\bmod m)(b\bmod m))\bmod m
$$

以加法为例,设$a=q_1m+r_1,b=q_2m+r_2$,那么:

$$
a+b=(q_1+q_2)m+(r_1+r_2)
$$

前面的整倍数部分不会影响余数,只需要继续计算$(r_1+r_2)\bmod m$。减法和乘法同理。

减法最容易漏掉负数。例如:

$$
(6-3)\bmod5=3
$$

但分别取余以后得到$1-3=-2$,还要再规范一次。

实际写题时,模数经常固定为$10^9+7$。如果$a,b$已经被规范到$[0,mod-1]$,可以把它设为默认模数:

1
2
3
4
5
const int MOD = 1e9 + 7;

int addM(int a, int b, int mod = MOD) { return (a += b) >= mod ? a - mod : a; }
int subM(int a, int b, int mod = MOD) { return (a -= b) < 0 ? a + mod : a; }
int mulM(int a, int b, int mod = MOD) { return (i64)a * b % mod; }

调用常用模数时不必反复传入MOD,遇到其他较小模数时再显式传入第三个参数。addMsubM只修正一次,是因为$a,b$已经在合法余数范围内;若输入可能为负数或超过模数,应先取模并处理负数。乘法先转成i64,避免$a\times b$在取模以前溢出。

这份简洁模板适用于模数不超过$10^9+7$的常见题目。如果模数来自输入并且可能接近i64上限,就不能继续使用int,应改回i64并用i128承接中间结果。后文遇到动态模数时会保留显式的mod参数。

除法不能照搬上面的运算律。例如:

$$
(12\div3)\bmod6=4
$$

但$12\bmod6=0$,先取模再除以$3$只会得到$0$。模意义下的除法需要乘法逆元,后面会专门说明。

同余

如果$a,b$除以$m$得到的余数相同,就称$a,b$模$m$同余,记作:

$$
a\equiv b\pmod m
$$

同余还可以写成更方便证明的形式:

$$
a\equiv b\pmod m\iff m\mid(a-b)
$$

它满足下面这些基本性质:

  1. 自反性:$a\equiv a\pmod m$;
  2. 对称性:若$a\equiv b\pmod m$,则$b\equiv a\pmod m$;
  3. 传递性:若$a\equiv b\pmod m$且$b\equiv c\pmod m$,则$a\equiv c\pmod m$;
  4. 若$a\equiv b\pmod m,c\equiv d\pmod m$,则$a+c\equiv b+d\pmod m$;
  5. 若$a\equiv b\pmod m,c\equiv d\pmod m$,则$ac\equiv bd\pmod m$;
  6. 若$a\equiv b\pmod m$,则$a^k\equiv b^k\pmod m$,其中$k$为非负整数。

同余式不能随意约分。若:

$$
ac\equiv bc\pmod m
$$

只有当$\gcd(c,m)=1$时,才能直接约去$c$并保持模数不变。一般情况下,设$d=\gcd(c,m)$,只能得到:

$$
a\equiv b\pmod{m/d}
$$

这也是模意义下的除法需要先判断互质条件的原因。

扩展欧几里得算法

裴蜀定理

对于不同时为$0$的整数$a,b$,一定存在整数$x,y$,使得:

$$
ax+by=\gcd(a,b)
$$

这就是裴蜀定理。更进一步,线性方程:

$$
ax+by=c
$$

存在整数解,当且仅当:

$$
\gcd(a,b)\mid c
$$

普通欧几里得算法只能求出最大公因数,扩展欧几里得算法还会顺便求出一组满足裴蜀等式的$x,y$。

从 gcd 推到 exgcd

欧几里得算法的递推式为:

$$
\gcd(a,b)=\gcd(b,a\bmod b)
$$

假设递归到下一层以后,已经求出:

$$
bx_1+(a\bmod b)y_1=g
$$

又因为:

$$
a\bmod b=a-\left\lfloor\frac ab\right\rfloor b
$$

代回原式可得:

$$
ay_1+b\left(x_1-\left\lfloor\frac ab\right\rfloor y_1\right)=g
$$

因此当前这一层的系数为:

$$
x=y_1,\qquad y=x_1-\left\lfloor\frac ab\right\rfloor y_1
$$

对应代码如下:

1
2
3
4
5
6
i64 exgcd(i64 a, i64 b, i64 &x, i64 &y) {
if (!b) { x = 1; y = 0; return a; }
i64 g = exgcd(b, a % b, y, x);
y -= a / b * x;
return g;
}

递归边界是$b=0$,此时:

$$
a\times1+0\times0=a
$$

回溯时再按照上面的式子逐层还原系数。整个过程与欧几里得算法的递归层数相同,对于非负且不同时为$0$的$a,b$,时间复杂度为$O(\log\max(a,b))$。

线性方程的全部解

设$g=\gcd(a,b)$,并且 exgcd 求出:

$$
ax_0+by_0=g
$$

如果$g\nmid c$,方程没有整数解;否则将两边同时乘$c/g$,得到一组特解:

$$
x_1=x_0\frac cg,\qquad y_1=y_0\frac cg
$$

全部整数解为:

$$
x=x_1+k\frac bg,\qquad k\in\mathbb Z
$$

$$
y=y_1-k\frac ag,\qquad k\in\mathbb Z
$$

如果题目只要求任意一组解,输出$x_1,y_1$即可;如果要求最小正整数解或限制解的范围,就需要继续调整$k$。

线性同余方程

形如:

$$
ax\equiv b\pmod m
$$

的式子称为线性同余方程。根据同余的定义,它等价于存在整数$y$,满足:

$$
ax+my=b
$$

设$d=\gcd(a,m)$。由裴蜀定理可知,方程有解当且仅当:

$$
d\mid b
$$

如果$d\nmid b$,直接判定无解;否则同时除以$d$:

$$
\frac adx\equiv\frac bd\pmod{m/d}
$$

此时$\gcd(a/d,m/d)=1$,所以$a/d$在模$m/d$意义下存在逆元。设$x_0$是一个解,那么原方程在模$m$意义下共有$d$个不同的解:

$$
x=x_0+k\frac md,\qquad k=0,1,\ldots,d-1
$$

如果只要求最小非负解,只需要把$x_0$规范到$[0,m/d-1]$。这一步很容易写错:解的周期是$m/d$,不是原来的$m$。

乘法逆元与欧拉定理

乘法逆元

若存在整数$x$满足:

$$
ax\equiv1\pmod m
$$

就称$x$是$a$在模$m$意义下的乘法逆元,记作$a^{-1}$。

逆元存在的充要条件是:

$$
\gcd(a,m)=1
$$

如果逆元存在,那么根据同余的定义,存在整数$y$使得:

$$
ax+my=1
$$

这正是裴蜀等式,所以可以使用 exgcd 求出$x$。反过来,如果$d=\gcd(a,m)>1$,那么$d$同时整除$ax$和$my$,不可能整除右边的$1$,逆元也就不存在。

求出$x$以后,还要把它规范到$[0,m-1]$:

1
2
3
i64 x, y;
i64 g = exgcd(a, mod, x, y);
if (g == 1) { x %= mod; if (x < 0) x += mod; }

有了逆元,模意义下的除法就可以改写为乘法:

$$
\frac ab\equiv a\cdot b^{-1}\pmod m
$$

但前提仍然是$\gcd(b,m)=1$。如果分母没有逆元,就不能使用这个式子。

欧拉函数与欧拉定理

欧拉函数$\varphi(m)$表示$1$到$m$中与$m$互质的整数个数。若$m$的不同质因子为$p_1,p_2,\ldots,p_k$,那么:

$$
\varphi(m)=m\prod_{i=1}^{k}\left(1-\frac1{p_i}\right)
$$

直接分解质因数即可在$O(\sqrt m)$时间内求出$\varphi(m)$。

当$\gcd(a,m)=1$时,欧拉定理给出:

$$
a^{\varphi(m)}\equiv1\pmod m
$$

因此可以对指数按$\varphi(m)$取模:

$$
a^b\equiv a^{b\bmod\varphi(m)}\pmod m
$$

互质条件不能省略。若$\gcd(a,m)>1$,直接把指数对$\varphi(m)$取模可能改变答案。

费马小定理

当$p$是质数时,$\varphi(p)=p-1$。若$p\nmid a$,欧拉定理可以写成:

$$
a^{p-1}\equiv1\pmod p
$$

这就是费马小定理。将两边拆出一个$a$:

$$
a\cdot a^{p-2}\equiv1\pmod p
$$

所以$a$在模$p$意义下的逆元为:

$$
a^{-1}\equiv a^{p-2}\pmod p
$$

指数$p-2$通常很大,需要用快速幂在$O(\log p)$时间内计算。

扩展欧几里得和费马小定理都可以求单个逆元,但适用条件不同:

方法 模数要求 时间复杂度 适合场景
扩展欧几里得 $\gcd(a,m)=1$ $O(\log m)$ 一般模数下的单个逆元
费马小定理 $p$必须是质数且$p\nmid a$ $O(\log p)$ 质数模数下的单个逆元

如果要求模质数$p$下$1$到$n$的全部逆元,并且$n<p$,还可以线性递推:

$$
inv_i=\left(p-\left\lfloor\frac pi\right\rfloor\right)inv_{p\bmod i}\bmod p
$$

1
2
inv[1] = 1;
for (int i = 2; i <= n; i++) inv[i] = mulM(mod - mod / i, inv[mod % i], mod);

这个写法可以在$O(n)$时间内求出全部逆元,不过它依赖$p$为质数且$n<p$,不能不看条件直接套用。

扩展欧拉定理

普通欧拉定理要求$a,m$互质。若题目没有这个保证,但指数$b$很大,可以使用扩展欧拉定理。

当$b<\varphi(m)$时,指数本身不需要处理;当$b\ge\varphi(m)$时:

$$
a^b\equiv a^{b\bmod\varphi(m)+\varphi(m)}\pmod m
$$

这里多加的$\varphi(m)$不能随手删掉。它保证底数与模数不互质时,约去的指数仍然足够大。

如果$b$大到必须用字符串保存,就在读入每一位时计算$b\bmod\varphi(m)$,同时记录$b$是否已经达到$\varphi(m)$。这样不需要真正存下$b$的数值。

常见模型与例题

下面七道题从快速幂开始,依次覆盖扩展欧几里得、线性同余方程、乘法逆元、费马小定理、扩展欧拉定理和任意模数下的等比数列求和。

题目 核心知识点 需要注意的地方
洛谷 P1226 快速幂 乘法溢出与输出格式
Codeforces 7C 扩展欧几里得 负数系数与无解判断
洛谷 P1516 线性同余方程 有解条件与最小非负解
洛谷 P1082 乘法逆元 将负数解规范为最小正解
AtCoder ABC156 D 费马小定理、组合数 $n$很大,不能预处理到$n$
洛谷 P5091 扩展欧拉定理 指数很长且模数不一定为质数
AtCoder ABC293 E 等比数列取模 模数不保证为质数,不能直接相除

例题一:洛谷 P1226 快速幂

题目链接:洛谷 P1226【模板】快速幂

给定$a,b,p$,求$a^b\bmod p$。

朴素做法连续乘$b$次,时间复杂度为$O(b)$。当$b$很大时,可以把它拆成二进制。例如:

$$
11=(1011)_2=8+2+1
$$

于是:

$$
a^{11}=a^8\cdot a^2\cdot a
$$

从低位到高位枚举$b$的二进制位。当前位为$1$时,把当前的$a$乘入答案;每处理完一位,就让$a$平方并把$b$右移一位。

1
2
3
4
5
6
7
i64 mulM(i64 a, i64 b, i64 mod) { return (i128)a * b % mod; }
i64 qpow(i64 a, i64 b, i64 mod) {
i64 ans = 1 % mod;
for (a %= mod; b; b >>= 1, a = mulM(a, a, mod))
if (b & 1) ans = mulM(ans, a, mod);
return ans;
}

循环次数等于$b$的二进制位数,因此时间复杂度为$O(\log b)$,空间复杂度为$O(1)$。

点击展开完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Fufffh */
#include <bits/stdc++.h>
using namespace std;
using i32 = int32_t; using i64 = int64_t; using i128 = __int128_t;
using u32 = uint32_t; using u64 = uint64_t; using u128 = __uint128_t;

i64 mulM(i64 a, i64 b, i64 mod) { return (i128)a * b % mod; }
i64 qpow(i64 a, i64 b, i64 mod) {
i64 ans = 1 % mod;
for (a %= mod; b; b >>= 1, a = mulM(a, a, mod))
if (b & 1) ans = mulM(ans, a, mod);
return ans;
}

int32_t main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
i64 a, b, p;
cin >> a >> b >> p;
cout << a << '^' << b << " mod " << p << '=' << qpow(a, b, p) << '\n';
return 0;
}

例题二:Codeforces 7C

题目链接:Codeforces 7C Line

题目给出$A,B,C$,要求寻找一组整数$x,y$,满足:

$$
Ax+By+C=0
$$

移项以后得到:

$$
Ax+By=-C
$$

设$g=\gcd(A,B)$,方程有整数解当且仅当$g\mid(-C)$。为了让 exgcd 的递归过程更稳定,可以先对$|A|,|B|$求系数,再按照原来的符号修正$x,y$。

exgcd 求出:

$$
|A|x_0+|B|y_0=g
$$

将$x_0,y_0$同时乘$(-C)/g$,就得到原方程的一组解。

点击展开完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* Fufffh */
#include <bits/stdc++.h>
using namespace std;
using i32 = int32_t; using i64 = int64_t; using i128 = __int128_t;
using u32 = uint32_t; using u64 = uint64_t; using u128 = __uint128_t;

i64 exgcd(i64 a, i64 b, i64 &x, i64 &y) {
if (!b) { x = 1; y = 0; return a; }
i64 g = exgcd(b, a % b, y, x);
y -= a / b * x;
return g;
}

int32_t main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
i64 a, b, c, x, y;
cin >> a >> b >> c;
c = -c;
i64 g = exgcd(abs(a), abs(b), x, y);
if (c % g) { cout << -1 << '\n'; return 0; }
x *= c / g; y *= c / g;
if (a < 0) x = -x;
if (b < 0) y = -y;
cout << x << ' ' << y << '\n';
return 0;
}

这道题中$A,B,C$都可能为负数,$A$或$B$还可能等于$0$。只要保证传入 exgcd 的两个数非负,并在最后恢复符号,就不需要为这些情况分别写一套算法。时间复杂度为$O(\log\max(|A|,|B|))$,空间复杂度为$O(\log\max(|A|,|B|))$。

例题三:洛谷 P1516 青蛙的约会

题目链接:洛谷 P1516 青蛙的约会

两只青蛙在长度为$L$的环上移动。它们的起点分别为$x,y$,每次分别移动$m,n$,求最少经过多少次移动才能相遇。

经过$t$次移动以后,两只青蛙的位置分别为:

$$
x+tm,\qquad y+tn
$$

要在环上相遇,两者模$L$同余:

$$
x+tm\equiv y+tn\pmod L
$$

移项得到:

$$
(m-n)t\equiv y-x\pmod L
$$

设$a=m-n,b=y-x,d=\gcd(a,L)$。当$d\nmid b$时无解;否则使用 exgcd 求出$a/d$在模$L/d$意义下的逆元,就能得到$t$的最小非负解。

点击展开完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* Fufffh */
#include <bits/stdc++.h>
using namespace std;
using i32 = int32_t; using i64 = int64_t; using i128 = __int128_t;
using u32 = uint32_t; using u64 = uint64_t; using u128 = __uint128_t;

i64 exgcd(i64 a, i64 b, i64 &x, i64 &y) {
if (!b) { x = 1; y = 0; return a; }
i64 g = exgcd(b, a % b, y, x);
y -= a / b * x;
return g;
}

int32_t main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
i64 x, y, m, n, l;
cin >> x >> y >> m >> n >> l;
i64 a = m - n, b = y - x, p, q;
i64 d = exgcd(abs(a), l, p, q);
if (b % d) { cout << "Impossible\n"; return 0; }
if (a < 0) p = -p;
i128 ans = (i128)p * (b / d) % (l / d);
if (ans < 0) ans += l / d;
cout << (i64)ans << '\n';
return 0;
}

这里最关键的是模数要缩小为$L/d$。时间复杂度和空间复杂度均为$O(\log L)$。

例题四:洛谷 P1082 同余方程

题目链接:洛谷 P1082 同余方程

题目要求同余方程:

$$
ax\equiv1\pmod b
$$

的最小正整数解。根据同余的定义,它等价于存在整数$y$,满足:

$$
ax+by=1
$$

输入保证方程有解,因此$\gcd(a,b)=1$。使用 exgcd 求出一组$x,y$以后,$x$可能为负数,但所有解在模$b$意义下属于同一个剩余类。将它规范为:

$$
x\leftarrow(x\bmod b+b)\bmod b
$$

就能得到$[0,b-1]$中的唯一代表。由于$b\ge2$且逆元存在,这个结果不会等于$0$,因此它正是题目要求的最小正整数解。

点击展开完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* Fufffh */
#include <bits/stdc++.h>
using namespace std;
using i32 = int32_t; using i64 = int64_t; using i128 = __int128_t;
using u32 = uint32_t; using u64 = uint64_t; using u128 = __uint128_t;

i64 exgcd(i64 a, i64 b, i64 &x, i64 &y) {
if (!b) { x = 1; y = 0; return a; }
i64 g = exgcd(b, a % b, y, x);
y -= a / b * x;
return g;
}

int32_t main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
i64 a, b, x, y;
cin >> a >> b;
exgcd(a, b, x, y);
x %= b;
if (x < 0) x += b;
cout << x << '\n';
return 0;
}

求单个逆元只需要一次扩展欧几里得,时间复杂度和空间复杂度均为$O(\log b)$。

例题五:AtCoder ABC156 D

题目链接:AtCoder ABC156 D Bouquet

有$n$种不同的花,每种只有一朵。需要选择至少一朵组成花束,但不能恰好选择$a$朵或$b$朵,求方案数对$10^9+7$取模的结果。

所有非空子集共有:

$$
2^n-1
$$

再减去大小恰好为$a,b$的集合:

$$
ans=2^n-1-\binom na-\binom nb
$$

这里$n$最大达到$10^9$,不能预处理到$n$的阶乘。但$a,b\le2\times10^5$,可以直接使用:

$$
\binom nk=\frac{n(n-1)\cdots(n-k+1)}{k!}
$$

模数$10^9+7$是质数,并且$k!$不被模数整除,所以:

$$
(k!)^{-1}\equiv(k!)^{mod-2}\pmod{mod}
$$

分子和分母分别计算,再用费马小定理求分母的逆元即可。

点击展开完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* Fufffh */
#include <bits/stdc++.h>
using namespace std;
using i32 = int32_t; using i64 = int64_t; using i128 = __int128_t;
using u32 = uint32_t; using u64 = uint64_t; using u128 = __uint128_t;
const int MOD = 1e9 + 7;

int subM(int a, int b, int mod = MOD) { return (a -= b) < 0 ? a + mod : a; }
int mulM(int a, int b, int mod = MOD) { return (i64)a * b % mod; }
int qpow(int a, i64 b, int mod = MOD) {
int ans = 1;
for (; b; b >>= 1, a = mulM(a, a, mod)) if (b & 1) ans = mulM(ans, a, mod);
return ans;
}
int comb(i64 n, int m) {
int a = 1, b = 1;
for (int i = 1; i <= m; i++) { a = mulM(a, (n - i + 1) % MOD); b = mulM(b, i); }
return mulM(a, qpow(b, MOD - 2));
}

int32_t main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
i64 n; int a, b;
cin >> n >> a >> b;
int ans = subM(subM(subM(qpow(2, n), 1), comb(n, a)), comb(n, b));
cout << ans << '\n';
return 0;
}

快速幂和两次组合数计算的总时间复杂度为$O(a+b+\log n+\log MOD)$,空间复杂度为$O(1)$。

例题六:洛谷 P5091 扩展欧拉定理

题目链接:洛谷 P5091【模板】扩展欧拉定理

给定$a,m,b$,求$a^b\bmod m$。其中$m$不保证为质数,$a,m$也不保证互质,并且$b$可能有两千万位,无法使用整数类型保存。

先分解$m$的质因数,求出$\varphi(m)$。随后逐位读取$b$,同时维护:

  1. $b\bmod\varphi(m)$;
  2. $b$是否已经达到$\varphi(m)$。

若$b<\varphi(m)$,快速幂的指数仍然是$b$;否则将指数改为:

$$
b\bmod\varphi(m)+\varphi(m)
$$

这样既保留了扩展欧拉定理需要的大小信息,也把真正参与快速幂的指数压到了$2\varphi(m)$以内。

点击展开完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/* Fufffh */
#include <bits/stdc++.h>
using namespace std;
using i32 = int32_t; using i64 = int64_t; using i128 = __int128_t;
using u32 = uint32_t; using u64 = uint64_t; using u128 = __uint128_t;

i64 mulM(i64 a, i64 b, i64 mod) { return (i128)a * b % mod; }
i64 phi(i64 n) {
i64 ans = n;
for (i64 p = 2; p * p <= n; p++) if (n % p == 0) {
ans = ans / p * (p - 1);
while (n % p == 0) n /= p;
}
if (n > 1) ans = ans / n * (n - 1);
return ans;
}
i64 qpow(i64 a, i64 b, i64 mod) {
i64 ans = 1 % mod;
for (a %= mod; b; b >>= 1, a = mulM(a, a, mod))
if (b & 1) ans = mulM(ans, a, mod);
return ans;
}
pair<i64, bool> parse(const string &s, i64 mod) {
i64 rem = 0, val = 0; bool large = false;
for (char c : s) {
int x = c - '0'; rem = (rem * 10 + x) % mod;
if (!large) { val = val * 10 + x; if (val >= mod) large = true; }
}
return {rem, large};
}

int32_t main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
i64 a, mod; string b;
cin >> a >> mod >> b;
i64 ph = phi(mod);
auto [e, large] = parse(b, ph);
if (large) e += ph;
cout << qpow(a, e, mod) << '\n';
return 0;
}

设$b$的十进制位数为$L$。求欧拉函数需要$O(\sqrt m)$,处理指数需要$O(L)$,最后一次快速幂需要$O(\log\varphi(m))$;空间复杂度为$O(L)$。

例题七:AtCoder ABC293 E

题目链接:AtCoder ABC293 E Geometric Progression

给定$A,X,M$,求:

$$
S_X=\sum_{i=0}^{X-1}A^i\bmod M
$$

普通等比数列公式为:

$$
S_X=\frac{A^X-1}{A-1}
$$

但题目没有保证$M$是质数,也没有保证$\gcd(A-1,M)=1$,所以$A-1$不一定存在逆元,不能直接使用这个分式。

可以在分治过程中同时维护:

$$
P_n=A^n\bmod M,\qquad S_n=\sum_{i=0}^{n-1}A^i\bmod M
$$

设已经得到$P_k,S_k$,那么:

$$
P_{2k}=P_k^2\pmod M
$$

$$
S_{2k}=S_k+A^kS_k=S_k(1+P_k)\pmod M
$$

如果$n=2k+1$,再补上最后一项$A^{2k}$:

$$
P_{2k+1}=P_{2k}A\pmod M
$$

$$
S_{2k+1}=S_{2k}+P_{2k}\pmod M
$$

每次递归都把$n$减半,不需要任何除法,也不要求$M$为质数。

点击展开完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* Fufffh */
#include <bits/stdc++.h>
using namespace std;
using i32 = int32_t; using i64 = int64_t; using i128 = __int128_t;
using u32 = uint32_t; using u64 = uint64_t; using u128 = __uint128_t;

i64 addM(i64 a, i64 b, i64 mod) { i128 x = (i128)a + b; return x >= mod ? x - mod : x; }
i64 mulM(i64 a, i64 b, i64 mod) { return (i128)a * b % mod; }
pair<i64, i64> calc(i64 a, i64 n, i64 mod) {
if (!n) return {1 % mod, 0};
auto [p, s] = calc(a, n >> 1, mod);
i64 p2 = mulM(p, p, mod), s2 = mulM(s, addM(p, 1 % mod, mod), mod);
if (!(n & 1)) return {p2, s2};
return {mulM(p2, a, mod), addM(s2, p2, mod)};
}

int32_t main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
i64 a, x, mod;
cin >> a >> x >> mod;
cout << calc(a % mod, x, mod).second << '\n';
return 0;
}

递归深度为$O(\log X)$,每层只进行常数次模乘,时间复杂度和空间复杂度均为$O(\log X)$。

如果还想继续练习,线性同余方程可以做AtCoder ABC186 E Throne,欧拉定理可以做AtCoder ABC222 G 222Codeforces 710D Two Arithmetic ProgressionsCodeforces 906D Power Tower会进一步涉及联立同余方程与欧拉函数链,适合放到后面再做。

总结与常见问题

常见问题

  • 为什么 C++ 中负数取模以后还可能是负数? C++ 的 % 保留被除数的符号;如果需要数学意义下的最小非负剩余,应先令 x%=mod,再在$x<0$时加上$mod$。
  • 为什么乘法以后再取模仍然可能出错? 如果 a*b 先发生整数溢出,后面的取模已经无法挽回。模数较大时应先转成 i128,或使用专门的快速乘。
  • 同余式两边能不能直接约分? 只有被约掉的数与模数互质时,才能保持原模数不变;否则需要缩小模数。
  • 线性同余方程什么时候有解? $ax\equiv b\pmod m$有解,当且仅当$\gcd(a,m)\mid b$。约去最大公因数以后,解的周期是$m/\gcd(a,m)$。
  • 逆元什么时候存在? $a$在模$m$意义下存在逆元,当且仅当$\gcd(a,m)=1$。
  • 扩展欧几里得和费马小定理应该用哪一个? 一般模数使用扩展欧几里得;模数确定为质数时,可以用快速幂计算$a^{p-2}$。
  • 费马小定理能不能用于合数模数? 不能直接使用$a^{m-2}$求逆元。模数不是质数时应先判断互质,再使用扩展欧几里得。
  • 指数能不能直接对$\varphi(m)$取模? 只有$a,m$互质时才能直接使用欧拉定理;不保证互质时,需要保留指数是否达到$\varphi(m)$的信息,再使用扩展欧拉定理。
  • 为什么求出的逆元可能是负数? 扩展欧几里得给出的是一组整数解,负数同样合法。对$x$取模,并在$x<0$时加上模数,就能得到最小非负代表。
  • 等比数列为什么不能直接套分式? 分母$A-1$在当前模数下可能没有逆元。不能确认互质时,应改用分治或矩阵快速幂。

复杂度分析

操作 时间复杂度 空间复杂度
单次加、减、乘与取模 $O(1)$ $O(1)$
快速幂 $O(\log b)$ $O(1)$
欧几里得算法 $O(\log\max(a,b))$ $O(1)$
扩展欧几里得 $O(\log\max(a,b))$ $O(\log\max(a,b))$
求解线性同余方程 $O(\log\max(\lvert a\rvert,m))$ $O(\log\max(\lvert a\rvert,m))$
扩展欧几里得求单个逆元 $O(\log m)$ $O(\log m)$
费马小定理求单个逆元 $O(\log p)$ $O(1)$
线性求$1$到$n$的逆元 $O(n)$ $O(n)$
质因数分解求$\varphi(m)$ $O(\sqrt m)$ $O(1)$
扩展欧拉处理$L$位指数 $O(\sqrt m+L+\log\varphi(m))$ $O(L)$
分治计算等比数列和 $O(\log X)$ $O(\log X)$

系列文章

  1. 基础数论入门
  2. 模意义下的数和运算