前言
12 和 18 有什么共同点?它们都能被 6 整除——6 是它们的最大公约数(GCD)。它们又都是 36 的因数——36 是它们的最小公倍数(LCM)。
这些看似简单的事实,在算法竞赛中无处不在。当你需要把一个分数化简、判断两个周期会不会同步、或者解模方程时,GCD 和 LCM 就是你的起点。而更深层的扩展欧几里得算法,不仅能算 GCD,还能帮你解方程 ——这是整个模运算体系的基石。
问题的本质
整除
如果 除以 余数为 0(即 ),我们说 整除 ,记作 。
整除有一个重要性质:如果 且 ,那么 和 。 这个性质是欧几里得算法的理论基础。
GCD 和 LCM
:能同时整除 和 的最大正整数。
:同时是 和 的倍数的最小正整数。
它们之间的关系:。
所以算 LCM 只需要先算 GCD:(先除后乘防溢出)。
理论 + 代码
欧几里得算法(辗转相除法)
。当 时,。特殊地,。
为什么这是对的? 更一般地, 对任意整数 成立。原因很简单:任何能同时整除 和 的数 ,也能整除 (因为 且 ,所以 )。反过来也对。所以 的公约数集合 的公约数集合,GCD 自然不变。
取 ,就得到 。
走一遍过程:。
| 步骤 | a | b | a mod b |
|---|---|---|---|
| 1 | 48 | 18 | 12 |
| 2 | 18 | 12 | 6 |
| 3 | 12 | 6 | 0 |
| 4 | 6 | 0 | —(b=0,答案是 6) |
4 步就得到 。
它有多快? 每次取余至少让较大的数减半(因为 ),所以两个数每两步就各缩小一半。总步数 ——即使 高达 ,也只需约 60 步。最坏情况发生在 Fibonacci 数列上(比如 需要 步),但 Fibonacci 数增长极快,所以实际不会太慢。
long long gcd(long long a, long long b) {
while (b) {
long long t = b;
b = a % b; // ① 取余数
a = t; // ② 原来的 b 变成新的 a
}
return a; // ③ b 为 0 时,a 就是 GCD
}
C++17 可以直接用 std::gcd(a, b),需要 #include <numeric>。
扩展欧几里得算法
不仅能算 ,还能找到整数 使得 。
这就是贝祖定理:对任意整数 ,存在整数 使得 。
long long exgcd(long long a, long long b, long long &x, long long &y) {
if (b == 0) {
x = 1; y = 0; // ① 边界:gcd(a,0) = a,a×1 + 0×0 = a
return a;
}
long long x1, y1;
long long g = exgcd(b, a % b, x1, y1); // ② 递归
x = y1; // ③ 回代
y = x1 - (a / b) * y1; // ④ 关键公式
return g;
}
走一遍过程:求 。
exgcd(48, 18)→ 递归exgcd(18, 12)exgcd(18, 12)→ 递归exgcd(12, 6)exgcd(12, 6)→ 递归exgcd(6, 0)exgcd(6, 0)→ ,返回 6- 回代到步骤 3:,。所以 。✓
- 回代到步骤 2:,。所以 。✓
- 回代到步骤 1:,。所以 。✓
答案是 :。
④ 为什么这样回代? 递归调用得到 。而 ,代入得 ,整理得 。所以 ,。
例题
例题 1:M&A 015 — Calculate GCD(★1)
题目:给定两个正整数 ,求 。
数据范围:
思路:直接用欧几里得算法(辗转相除法)。,要用 long long。
代码:
#include <cstdio>
using namespace std;
long long gcd(long long a, long long b) {
while (b) { // ① b 不为 0 时继续
long long t = b;
b = a % b; // ② 取余
a = t; // ③ 交换
}
return a;
}
int main() {
long long A, B;
scanf("%lld%lld", &A, &B);
printf("%lld\n", gcd(A, B));
}
逐行解析:
- ①②③ 欧几里得算法的核心:。每一步 至少减半,所以循环次数 。
例题 2:M&A 016 — Greatest Common Divisor of N Integers(★2)
题目:给定 个正整数 ,求它们的最大公约数。
数据范围:,
思路:。从左到右依次求 GCD。
代码:
#include <cstdio>
using namespace std;
long long gcd(long long a, long long b) {
while (b) { long long t = b; b = a % b; a = t; }
return a;
}
int main() {
int N;
scanf("%d", &N);
long long g = 0; // ① gcd(0, x) = x
for (int i = 0; i < N; i++) {
long long a;
scanf("%lld", &a);
g = gcd(g, a); // ② 依次合并
}
printf("%lld\n", g);
}
逐行解析:
- ① 初始值
g = 0。因为 (0 能被任何数整除),所以第一次gcd(0, A[0]) = A[0]。 - ② 每读入一个数就和当前的 GCD 合并。
例题 3:M&A 017 — Least Common Multiple of N Integers(★2)
题目:给定 个正整数,求它们的最小公倍数。
数据范围:,
思路:。先除后乘防溢出。同样从左到右依次合并。
代码:
#include <cstdio>
using namespace std;
long long gcd(long long a, long long b) {
while (b) { long long t = b; b = a % b; a = t; }
return a;
}
int main() {
int N;
scanf("%d", &N);
long long l = 1; // ① 初始值 1
for (int i = 0; i < N; i++) {
long long a;
scanf("%lld", &a);
l = l / gcd(l, a) * a; // ② 先除后乘!
}
printf("%lld\n", l);
}
逐行解析:
- ②
l / gcd(l, a) * a:先除 再乘 。如果写成l * a / gcd(l, a),中间值可能溢出long long。
例题 4:M&A 072 — Max GCD 2(★3)
题目:给定两个整数 (),在 到 之间选两个不同的整数,使 GCD 最大。输出最大 GCD。
数据范围:
思路:如果最大 GCD 是 ,那么区间 中至少有两个 的倍数。最大的 满足 ,即 ,等价于 (当 时, 和某个 的倍数都在 中)。
所以答案就是 当 时?不对。更准确的:答案就是满足 且区间中至少有两个 的倍数的最大 。
简单做法:从 倒着找第一个满足条件的 。
代码:
#include <cstdio>
using namespace std;
int main() {
int A, B;
scanf("%d%d", &A, &B);
int ans = 1;
for (int g = B - A; g >= 1; g--) { // ① 从大到小枚举 g
// ② 检查 [A, B] 中是否有至少两个 g 的倍数
int cnt = B / g - (A - 1) / g; // [A,B] 中 g 的倍数个数
if (cnt >= 2) {
ans = g;
break;
}
}
printf("%d\n", ans);
}
逐行解析:
- ① 从 开始往小枚举。因为如果 ,则 中最多只有一个 的倍数(相邻 的倍数间距为 )。
- ②
B / g - (A - 1) / g是经典的区间中倍数个数的公式。
例题 5:M&A 013 — Divisor Enumeration(★1)
题目:给定整数 ,枚举 的所有约数。
数据范围:
思路:从 1 到 试除,每找到一个约数 就同时得到 。这道题在 00-02 排序 中已经用排序输出做过。这里强调整除性的角度。
代码(与 00-02 相同):
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
long long N;
scanf("%lld", &N);
vector<long long> divs;
for (long long i = 1; i * i <= N; i++) {
if (N % i == 0) {
divs.push_back(i);
if (i != N / i)
divs.push_back(N / i);
}
}
sort(divs.begin(), divs.end());
for (auto d : divs)
printf("%lld\n", d);
}
参考文献
理论讲义 — AtCoder Algorithm Lectures
教材讲解 — 競技プログラミングの鉄則 第 5 章
基础练习 — アルゴリズムと数学 演習問題集
- 015 Calculate GCD(GCD基础)【例题】
- 016 Greatest Common Divisor of N Integers【例题】
- 017 Least Common Multiple of N Integers【例题】
- 072 Max GCD 2(区间最大GCD)【例题】
- 013 Divisor Enumeration(约数枚举)【例题】
- 014 Factorization(素因数分解)
- 042 Sum of Divisors(约数函数求和)
- 093 Large LCM(★3,LCM溢出判断)
系统练习 — 競技プログラミングの鉄則
系列索引
第零章 基础工具
第一章 搜索技术
第二章 数学基础
第三章 数据结构
- 03-01 栈、队列与单调栈
- 03-02 堆与优先队列
- 03-03 并查集
- 03-04 树状数组
- 03-05 线段树
- 03-06 懒标记线段树
- 03-07 Sparse Table 与倍增
- 03-08 字符串哈希
第四章 图论
- 04-01 图的遍历
- 04-02 最短路—Dijkstra 与 01-BFS
- 04-03 最短路—Bellman-Ford 与 Floyd
- 04-04 拓扑排序
- 04-05 最小生成树
- 04-06 强连通分量与 2-SAT
- 04-07 二分图与网络流
- 04-08 树上问题
第五章 动态规划
- 05-01 DP入门—状态与转移
- 05-02 背包问题族
- 05-03 LIS、LCS与编辑距离
- 05-04 区间DP
- 05-05 状态压缩DP
- 05-06 树形DP与数位DP
- 05-07 矩阵快速幂与线性递推
第六章 贪心
第七章 字符串
第八章 进阶