上一节把数拆成质数,这一节处理“两个数的公共结构”和“大指数”。三件事:两个数的最大公约数与最小公倍数、同余方程与逆元、ab mod m。第三件是第二件的工具,第二件是第一件的推广——三者用同一套代码互相支撑。
| 要做的事 | 数据规模 | 用什么 | 复杂度 |
|---|---|---|---|
| 两个数的 gcd | a, b ≤ 10^18 | 辗转相除 | O(log min(a,b)),≤ 95 步 |
| 两个数的 lcm | 结果 ≤ 9.2×10^18 | a / gcd * b,先除后乘 | 同上 |
| a·x ≡ 1 (mod m) 的逆元 | m ≤ 10^9 | 扩展欧几里得 / 费马小定理 | O(log m) |
| a^b mod m | b ≤ 10^18 | 快速幂 | O(log b) |
| 两个 10^18 的数相乘再取模 | mod ≤ 10^18 | __int128 | O(1) |
先把这几个工具拆开看清。它们都很短,但每一个都有一处“不这么写就错”的细节,这些细节正是这一节真正的考点。
| 组成 | 含义 / 格式要求 |
|---|---|
头文件 | <numeric>。<bits/stdc++.h> 已经包含,不用额外写。 |
两个参数类型必须相同 | gcd(3, 4LL) 编译不过(模板推导冲突)。竞赛里统一用 long long 最省事。 |
返回值 | 非负。gcd(−12, 18) = 6,它内部按绝对值算。 |
gcd(x, 0) = |x| | 辗转相除的终止条件正好是 b = 0,所以 0 是合法参数,不用特判。 |
C++17 之前 | 只能手写辗转相除,或用 GNU 扩展 __gcd(a, b)(非标准,MSVC 没有)。 |
手写版 | 四行:while (b) { t = a % b; a = b; b = t; } return a; 逻辑与递归版完全一样。 |
#include <bits/stdc++.h>
using namespace std;
int main() {
cout << gcd(12, 18) << "\n"; // 6
cout << gcd(0, 7) << "\n"; // 7:gcd(0, x) = |x|
cout << gcd(-12, 18)<< "\n"; // 6:取绝对值
long long a = 1000000000000LL, b = 8;
cout << gcd(a, b) << "\n"; // 4:两个都是 long long
// 手写辗转相除(不依赖 C++17)
long long x = a, y = b;
while (y) { long long t = x % y; x = y; y = t; }
cout << x << "\n"; // 4
return 0;
}gcd(3, 4LL) 这类“一个 int 一个 long long”的写法直接编译失败(no matching function)。出错时先看参数的声明类型。
手写版若 a、b 可能为负数,先各自取绝对值:C++ 的 % 对负数返回负余数,不处理会死循环。
| 组成 | 含义 / 格式要求 |
|---|---|
a * b / gcd | 错。a = b = 4×10^9 时 a·b = 1.6×10^19,超出 long long 上限 9.22×10^18,先溢出再除,结果毫无意义。 |
a / gcd * b | 对。gcd 一定整除 a,先除不丢精度;中间值最大就是最终答案,不会比 lcm 更大。 |
为什么顺序有效 | 公因子被提前消掉了。两个数相乘只做一次,谁先谁后不影响结果,只影响中间值的大小。 |
std::lcm 也存在 | C++17 提供,同样先除后乘;但竞赛里更常手写——模意义下的 lcm 要乘逆元,标准库帮不上。 |
返回值 | lcm(a, b) = a / gcd(a, b) * b 对正整数恒成立;带负数时先取绝对值。 |
装不下的情况 | lcm 本身超过 9.2×10^18 时,任何 long long 写法都没救,要么题目保证范围,要么上 __int128。 |
long long lcm(long long a, long long b) {
return a / gcd(a, b) * b; // 先除后乘
}
// 反例:a = b = 4e9
// a * b / gcd = 1.6e19 / 4e9 → 1.6e19 已超出 long long,溢出成负数
// a / gcd * b = 1e9 * 4e9 → 4e18,正确写乘法前先估一次中间值。这是数论题里最常见、也最难查的一类 WA:答案小的时候全对,大测试点突然变成负数。
a ≥ b 时先算 a / gcd 得到的商更小,习惯上把大的放前面。
| 组成 | 含义 / 格式要求 |
|---|---|
范围 | 约 ±1.7×10^38。两个 10^18 的数相乘得 10^36,装进去还剩两位数量级的余量。 |
不是标准类型 | GCC / Clang 支持(洛谷、Codeforces、NOI 系列评测机都是 GCC),MSVC 不支持。别名 __int128_t。 |
不能直接读入输出 | cin、cout、scanf、printf 全不认识它。读入先读进 long long 再赋值;输出要手写按位打印。 |
转换要写在乘法之前 | (__int128)a * b 正确:a 先被提升,b 自动跟着提升。(__int128)(a * b) 错误:乘法已经在 long long 里溢出,再转也救不回来。 |
速度 | 加减与 long long 接近,乘除取模慢 3~10 倍。10^8 次循环里慎用。 |
典型用法 | 把一次可能溢出的乘法“抬”到 128 位算完,取模后降回 long long。 |
// ① 防溢出的乘法取模:a、b < m ≤ 10^18
long long mulMod(long long a, long long b, long long m) {
return (long long)((__int128)a * b % m); // 中间 10^36,long long 装不下
}
// ② 打印 __int128:cout 不认识它,只能自己按位输出
void print128(__int128 x) {
if (x < 0) { putchar('-'); x = -x; }
if (x > 9) print128(x / 10);
putchar('0' + (int)(x % 10));
}(long long)(...) 这一步不能省:表达式类型是 __int128,赋给 long long 变量时虽会自动截断,但显式写出来更清楚,也避免 cout 直接输出它。
本节题目 k ≤ 2^31,(2^31)² = 4.6×10^18 还在 long long 内,其实不用 128 位;但模板写成 __int128 版本可以无脑复用,不必每次重新估范围。
| 组成 | 含义 / 格式要求 |
|---|---|
a | 底数。进门先 a %= mod,否则第一次乘法就可能用到大数。 |
b | 指数。必须用 long long:2^31 = 2147483648 已经超过 int 上限 2147483647。 |
mod | 模数。mod = 1 时任何数都同余于 0,结果必须是 0。 |
r = 1 % mod | 累乘器初值。直接写 1 在 mod = 1 且 b = 0 时会返回 1,答案应为 0;写 1 % mod 一劳永逸。 |
b & 1 | 取 b 的最低位。为 1 表示当前这份 a^(2^k) 要乘进答案。 |
a = a * a % mod | a 依次变成 a2、a4、a8……每轮平方一次。 |
b >>= 1 | 右移一位,处理下一个二进制位。循环次数 = b 的二进制位数 ≤ 63。 |
long long qpow(long long a, long long b, long long mod) {
long long r = 1 % mod; // 处理 mod = 1 的退化情况
a %= mod;
while (b) {
if (b & 1) r = r * a % mod; // 这一位是 1:把 a^(2^k) 乘进来
a = a * a % mod; // a 平方,准备下一位
b >>= 1;
}
return r;
}
// qpow(2, 10, 9) = 1024 % 9 = 7
// qpow(2, 0, 1000) = 1
// qpow(7, 5, 1) = 0每一步都要取模。写成 r = r * a 最后统一取模,b ≥ 40 就溢出,症状是中等数据突然错、大数据变成负数。
mod ≤ 2^31 时 r·a 不超过 4.7×1018,long long 够用;mod 到 10^18 就必须把两次乘法换成 __int128。
输入三个整数 b、p、k(1 ≤ b, p, k ≤ 231),求 bp mod k 的值。
输出格式严格为 b^p mod k=s,其中 s 是结果,等号两边没有空格。
注意:p 最大 231,用 int 读入会失败。
先算朴素做法要多久:循环 p 次做一次乘法,p ≤ 231 = 21 亿,就算每纳秒一次乘法也要 2 秒以上,必然 TLE。所以必须减少乘法次数,而不是优化每一步。
关键观察:p 的二进制最多 31 位,p 就是若干个 2 的幂之和。于是 b^p 等于对应的那些 b^(2^k) 相乘——只要手里有 b^1、b^2、b^4、…、b^(2^30) 这 31 个数,按 p 的二进制位挑出来相乘即可。
这 31 个数不用分别算:后一个是前一个的平方。(b^2)^2 = b^4,(b^4)^2 = b^8,一路平方下去,正好 31 次。
取模可以随时插入:因为 (x·y) mod m = ((x mod m)·(y mod m)) mod m。每一次乘法后立刻取模,中间值就永远不超过 m² ≤ 4.6×1018,long long 装得下。
r = 1, a = b % k
当 p > 0:
若 p 的最低位是 1:r = r * a % k ← 这一位对应的幂要乘进来
a = a * a % k ← 幂次翻倍:a, a², a⁴, ...
p >>= 1 ← 右移,看下一位
输出 r两处细节必须写在代码里,否则有测试点过不了:p 用 long long 读(231 超 int);输出用原始的 b、p、k(不是在函数里取过模的副本)。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// 计算 a^b mod m
ll qpow(ll a, ll b, ll m) {
ll r = 1 % m; // m = 1 时答案恒为 0,1 % 1 = 0
a %= m; // 底数先进模,后面的乘法才安全
while (b) {
if (b & 1) r = (__int128)r * a % m; // 这一位是 1,把当前的 a^(2^k) 乘进答案
a = (__int128)a * a % m; // 平方,得到下一份的幂
b >>= 1; // 处理下一个二进制位
}
return r;
}
int main() {
ll b, p, k;
cin >> b >> p >> k; // p 可达 2^31,必须 long long
cout << b << "^" << p << " mod " << k << "=" << qpow(b, p, k) << "\n";
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
ll r = 1 % m; | 累乘器初值。写 1 的话,m = 1 且 b = 0 时返回 1,而正确答案是 0。 |
a %= m; | 底数先取模。否则 b 的最低位就是 1 时,第一轮 r * a 直接用到大数,乘完再取模已经溢出。 |
while (b) | b 每轮右移一位,循环次数等于 b 的二进制位数,p ≤ 2^31 时最多 31 轮。 |
if (b & 1) | 取最低位。这一位是 1 表示 a^(2^k) 属于乘积,要乘进 r;是 0 就跳过。 |
(__int128)r * a % m | 抬起一轮再取模。m 到 10^18 时 r·a 会溢出 long long,这样写一劳永逸。 |
a = (__int128)a * a % m; | 平方。注意这里用的是取模后的 a,与 r 无关,两行互不干扰。 |
b >>= 1; | 右移一位。写成 b /= 2 结果一样,移位更贴合“二进制位”的语义。 |
cin >> b >> p >> k; | 三个变量都是 long long:p 最大 2^31 = 2147483648,int 上限只有 2147483647,用它读会失败。 |
cout << b << "^" ... | 输出的是原始 b、p、k,不是函数里取过模的副本。格式里 ^ 和 = 两边都没有空格。 |
快速幂解决“指数很大怎么算”。数论的另一半问题是“除法怎么取模”。加减乘都可以边算边取模,只有除法不行:(a / b) mod m 既不等于 (a mod m) / (b mod m),很多时候压根不是整数。要让“除以 b”变成“乘以 b 的逆元”,就得解同余方程 b·x ≡ 1 (mod m)。
| 运算 | 能否直接取模 | 正确写法 |
|---|---|---|
| (a + b) mod m | 能 | (a mod m + b mod m) mod m |
| (a − b) mod m | 能 | ((a mod m − b mod m) + m) mod m,先加 m 防负 |
| (a × b) mod m | 能 | 快速幂每步取模的依据;中间乘积超范围就上 __int128 |
| (a ÷ b) mod m | 不能 | 乘以 b 的逆元 b−1,前提是 gcd(b, m) = 1 |
求逆元要先会解 a·x + b·y = gcd(a, b)。裴蜀定理保证这个方程一定有整数解,而把辗转相除的每一步“回代”就能把解构造出来——这就是扩展欧几里得。
// 求 a*x + b*y = gcd(a,b) 的一组整数解,返回 gcd(a,b)
// x、y 用引用传递,返回值通过参数带出(第 11 节学过)
long long exgcd(long long a, long long b, long long &x, long long &y) {
if (b == 0) { // 出口:a*1 + 0*0 = a
x = 1; y = 0;
return a;
}
long long x1, y1;
long long g = exgcd(b, a % b, x1, y1); // 先让规模变小,再回代
x = y1; // a 的系数
y = x1 - (a / b) * y1; // b 的系数
return g;
}| 代码片段 | 含义与理由 |
|---|---|
if (b == 0) { x = 1; y = 0; return a; } | 出口。此时 a 就是 gcd(a, b),取 x = 1、y = 0 满足 a·1 + 0·0 = a。 |
exgcd(b, a % b, x1, y1) | 规模从 (a, b) 变成 (b, a mod b),和辗转相除走的是同一条路,所以递归深度也同阶。 |
b·x1 + (a mod b)·y1 = g | 递归返回时我们手里有这一条等式。 |
a mod b = a − (a / b)·b | 把上式里的 a mod b 展开:b·x1 + (a − (a/b)·b)·y1 = a·y1 + b·(x1 − (a/b)·y1)。 |
x = y1; | 对照展开后的式子,a 的系数是 y1,所以新的 x 就是 y1。 |
y = x1 - (a / b) * y1; | b 的系数是 x1 − (a/b)·y1,这就是新的 y。 |
返回 g | 顺手拿到 gcd,省得再调一次,也是判断“方程有没有解”的依据。 |
拿到 exgcd 之后,逆元就是一行的事:a·x + m·y = 1 里的 x 就是 a 模 m 的逆元。另一种求法是把上一节的快速幂接上来——当 m 是质数时,费马小定理直接给答案。
// ① 扩展欧几里得:任意模数,只要 gcd(a, m) == 1 就有逆元
long long invExgcd(long long a, long long m) {
long long x, y;
long long g = exgcd(a, m, x, y); // a*x + m*y = gcd(a, m)
if (g != 1) return -1; // 不互质,逆元不存在
return (x % m + m) % m; // 归一化到 [0, m)
}
// ② 费马小定理:只在 m 是质数时可用(1e9+7、998244353 这类)
long long invFermat(long long a, long long m) {
return qpow(a, m - 2, m); // a^(m-2) ≡ a^(-1) (mod m)
}| 代码片段 | 含义与理由 |
|---|---|
a·x + m·y = gcd(a, m) | 裴蜀定理:a·x + m·y 能取到的最小正数恰是 gcd(a, m)。所以方程的右边必须能被 gcd 整除。 |
if (g != 1) return -1; | 要让右边等于 1,只有 gcd = 1。这就是“逆元存在 ⟺ 互质”的来源。 |
(x % m + m) % m | C++ 的 % 结果符号跟着被除数走,负数取模仍是负数。先 %m 把它拉进 (−m, m),加 m 变正,再 %m 收进 [0, m)。 |
qpow(a, m - 2, m) | 费马小定理:m 为质数且 m ∤ a 时 a^(m−1) ≡ 1,两边同除 a 得 a^(m−2) ≡ a^(−1)。 |
两种做法的取舍 | 费马版是 log m 次乘法,常数小、代码短;exgcd 版任意模数都能用。m 是 1e9+7 时竞赛里普遍用费马版。 |
WA,答案恒为 0。—— 快速幂的累乘器写成 long long r = 0;。任何数乘 0 都是 0,样例就能看出来。
WA,小数据全对、大数据错误或为负。—— 每步忘了取模,中间值 40 次乘法后溢出。凡是“取模题答案忽正忽负”,先查这一步。
TLE 或输出随机值。—— 指数 p 用 int 读 2^31。cin 解析失败后 p 保持未初始化值,循环次数完全随机,表现为 TLE 或答案离谱。指数、模数一律 long long。
WA,输出负数。—— 逆元直接写 x % m。样例 3 10 恰好得到正数 7 看不出来,换 a = 2、b = 3 会输出 −1(正确答案 2)。必须 (x % m + m) % m。
WA,答案比正确值小且是负数。—— lcm 写成 a * b / gcd(a, b),a、b 到 4×10^9 时乘积溢出。
RE(除零)或无限递归。—— 手写 gcd 的出口写成 if (b == 1) return 1;。该停的时候 b 已经变成 0,下一轮还要算 a % b,除零 RE。必须是 if (b == 0)。
CE(编译失败)。—— gcd(3, 4LL) 类型不一致;或者用 cin >> x 读 __int128。前者加一个 LL 后缀统一类型,后者只能先读 long long 再赋值。
WA,本应输出“无解”却输出了一个数。—— 同余方程 a·x ≡ b (mod m) 在 gcd(a, m) ∤ b 时无解,但 exgcd 照样会返回 a·x + m·y = g 的一组解,直接拿来用就是错的。先判 b % g != 0 再谈求解。
关掉页面,从空文件把 P1226 敲一遍并提交到洛谷。
改一改 ①:把 while 循环换成 for (ll i = 0; i < p; ++i) r = r * b % k;,输入 2 1000000000 998244353,用 time 计时——亲眼看一次 10 亿次乘法要多久。
改一改 ②:把 if (b & 1) 改成 if (b & 2),输入 2 10 1000,看答案从 24 变成多少。想清楚为什么只“少乘了某些位”。
改一改 ③:把 lcm 写成 a * b / gcd(a, b),输入 a = b = 4000000000,打印中间值 a*b——观察它变成负数,再换回 a / gcd * b 对比。
改一改 ④:用 exgcd 版求逆元,把 (x % m + m) % m 改成 x % m,输入 a = 2、m = 3,输出会变成 −1。这就是“最小正整数解”必须归一化的原因。
同余的定义只有一句:a ≡ b (mod m) 表示 m 整除 a − b。它把整数按“除以 m 的余数”分成 m 类,而所有运算都在这 m 个类上进行——这就是“模 m 剩余系”。
为什么加减乘可以随便取模:若 a ≡ a′、b ≡ b′ (mod m),则 a ± b ≡ a′ ± b′、a·b ≡ a′·b′。所以每一步乘完立刻取模,结果不变。这也解释了快速幂为什么能边乘边模。
除法为什么不行:2 ≡ 8 (mod 6),两边同除 2 得 1 与 4,而 1 ≢ 4 (mod 6)。取模把“差 m 的倍数”这个信息扔掉了,而除法恰好要用到它。补救办法是乘逆元。
裴蜀定理:对任意整数 a、b(不全为 0),存在整数 x、y 使 a·x + b·y = gcd(a, b),而且 gcd(a, b) 是所有形如 a·x + b·y 的正数中最小的那个。
通解结构:若 (x0, y0) 是 a·x + b·y = g 的一组解,则全部解为 x = x0 + k·(b/g)、y = y0 − k·(a/g),k 取遍整数。所以对 b 取模后逆元唯一——这就是 (x % m + m) % m 一定对的理由。
费马小定理:p 为质数且 p ∤ a,则 ap−1 ≡ 1 (mod p),于是 a−1 ≡ ap−2。模 109+7 求逆元时 p − 2 = 1000000005,一次快速幂 30 轮乘法搞定。(更一般的是欧拉定理 aφ(m) ≡ 1,条件只有 gcd(a, m) = 1。)
量级感受:gcd 的递归次数不超过 5·log10(min(a, b))(拉梅定理),a、b ≤ 2×109 时不超过 50 层,绝无爆栈风险;快速幂的轮数等于指数的二进制位数,b ≤ 1018 时 60 轮。两个算法在 1018 的量级上都只要几十步——这就是“对数级”的含义。
求关于 x 的同余方程 a·x ≡ 1 (mod b) 的最小正整数解。
输入:一行两个正整数 a、b(2 ≤ a, b ≤ 2×109),用一个空格隔开。
输出:一行一个正整数 x0,即最小正整数解。输入数据保证一定有解。
把“a·x 除以 b 余 1”翻译成等式:a·x − 1 是 b 的倍数,也就是存在整数 y 使 a·x + b·y = 1。
求 a·x + b·y = gcd(a, b) 的一组解是扩展欧几里得的标准任务。先想清楚:本题里 gcd(a, b) 必须等于几,方程才有解?
解出来的 x 可能为负、也可能大于 b。怎么把它挪进 1 到 b−1 之间?注意 C++ 里负数取模的结果。
a·x ≡ 1 (mod b) 等价于 b | (a·x − 1),即存在整数 y 使 a·x − 1 = b·y,也就是 a·x + b·(−y) = 1。令 y′ = −y,问题变成解 a·x + b·y′ = 1。
由裴蜀定理,a·x + b·y 能取到的最小正数恰是 gcd(a, b)。题目保证有解,所以 gcd(a, b) 只能是 1。
调用 exgcd(a, b, x, y),返回时 x 满足 a·x + b·y = gcd(a, b) = 1,它就是一组解。
通解是 x + k·b:因为 a·(x + k·b) = a·x + k·a·b ≡ a·x (mod b)。所以把所有解对 b 取模,落进 [0, b) 的那个就是唯一答案。
C++ 的 % 对负数返回负余数,必须写成 (x % b + b) % b。又因为 b ≥ 2 且 a·x ≡ 1 ≠ 0,所以结果不会等于 0,落在 1..b−1,正是最小正整数解。
复杂度:与辗转相除同阶,b ≤ 2×109 时递归不超过 50 层,O(log b)。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// 求 a*x + b*y = gcd(a,b) 的一组整数解,返回 gcd(a,b)
ll exgcd(ll a, ll b, ll &x, ll &y) {
if (b == 0) { // 出口:a*1 + 0*0 = a
x = 1; y = 0;
return a;
}
ll x1, y1;
ll g = exgcd(b, a % b, x1, y1); // 规模先变小
x = y1; // 回代:a 的系数
y = x1 - (a / b) * y1; // 回代:b 的系数
return g;
}
int main() {
ll a, b, x, y;
cin >> a >> b;
exgcd(a, b, x, y); // 得到 a*x + b*y = gcd(a,b) = 1
x = (x % b + b) % b; // 归一化到 [0, b):先 % 再 +b 再 %
cout << x << "\n";
return 0;
}| 片段 | 作用 |
|---|---|
ll exgcd(ll a, ll b, ll &x, ll &y) | x、y 用引用传递,答案靠它们带出;返回值是 gcd。a、b 都是 long long,因为上限 2×10^9 已经贴着 int 的边缘。 |
if (b == 0) { x = 1; y = 0; return a; } | 递归出口,也就是辗转相除的最后一步:此时 gcd = a,取 x = 1、y = 0 使 a·1 + 0·0 = a 成立。 |
exgcd(b, a % b, x1, y1) | 把 (a, b) 换成 (b, a mod b)。这是整个算法唯一“变小”的地方,也是它必然终止的原因。 |
x = y1; y = x1 - (a / b) * y1; | 回代。由 b·x1 + (a − (a/b)·b)·y1 = g 直接对照系数得到,不能写反。 |
x = (x % b + b) % b; | ★ 归一化。exgcd 给的解可能为负(例如 a = 2、b = 3 时 x = −1),负余数仍是负数,必须加 b 再取模才能落进 [0, b)。 |
cout << x | b ≥ 2 且 a·x ≡ 1 (mod b),所以 x 不会是 0;归一化后的值就是最小正整数解。 |
x % b 当成“取最小正数”:C++ 的余数跟随被除数符号,负数取模还是负数。x = y1; y = x1 - (a/b)*y1; 里的 x1、y1 对调,样例 3 10 立刻输出 1 而不是 7。这两行必须从 a mod b 的展开式自己推一遍。exgcd(b, a % b, y, x)(另一种紧凑写法)却照抄上面两行回代公式——两种写法必须成套使用,混用会得到错误的符号。a / b、乘法都贴着 int 上限,统一 long long 更安全。