前言:整数的“原子结构”
你可能很早就学过素数、公约数这些概念。在日常生活中,知道”6 和 15 的最大公约数是 3”就够了。但在算法题里,这些概念变成了工具——你需要用它们来解决问题。
举几个例子:
- “把 个糖果平均分给 个人,每人最多分几个,还剩几个?“——这是取模运算,但你可能还需要知道剩余的糖果能不能再平均分下去,这就涉及了 。
- “判断一个大数是不是素数”——直接试除太慢了,有没有更快的方法?
- “从 1 到 中,有多少个数和 互素?“——这引出了欧拉函数,它是密码学(比如 RSA)的基石。
这篇笔记的核心线索是整数的分解。就像物质由原子组成一样,每个整数都可以唯一分解为素数的乘积(算术基本定理)。围绕这个事实,我们能推导出一系列有用的工具:
- GCD 与辗转相除 —— 怎么快速求最大公约数
- 素数筛 —— 怎么快速找出所有素数
- 欧拉函数 —— 怎么快速算出”与 互素的数有多少个”
- 约数计数 —— 怎么从素因子分解出发计算约数个数和约数和
这些工具看起来零散,但它们都建立在同一个基础之上:整数的素因子分解。最后我们会用一道综合题,把这些工具串起来。
一、最大公约数与最小公倍数
1.1 辗转相除法(欧几里得算法)
每步至少把较大的数减半,所以迭代次数 。
long long gcd(long long a, long long b) {
return b ? gcd(b, a % b) : a;
}
C++17 起标准库有 std::gcd,但理解原理是必要的——因为很多扩展算法直接基于它。
1.2 为什么 ?
设 。则 且 。而 ,所以 也整除 。反过来,任何同时整除 和 的数也整除 。所以两对数的公约数集合完全相同,最大值自然相同。
1.3 LCM
注意先除后乘防溢出:a / gcd(a, b) * b。
1.4 扩展欧几里得算法
上面我们只是计算了 。但有时候我们需要知道更多信息——比如” 和 的 能不能表示成 的形式?“答案是可以,而且辗转相除的过程中可以顺便求出 和 。
递归推导:假设已知 对应的解 ,即 。代入 :
所以 ,。
long long exgcd(long long a, long long b, long long &x, long long &y) {
if (b == 0) { x = 1; y = 0; 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 解)、解线性同余方程、解线性丢番图方程。
二、素数
2.1 定义与基本性质
素数是大于 1 且恰好有两个正因子(1 和自身)的整数。
- 前 20 个素数:2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71
- 算术基本定理:每个 的整数可以唯一地分解为素数的乘积
- 小于 的素数约有 个(素数定理)
2.2 埃拉托斯特尼筛法
要判断一个数是否为素数,可以试除到 。但如果需要知道 到 中所有素数呢?逐个试除太慢了。筛法的思路完全不同:不问”这个数是不是素数”,而是”把所有合数标记出来,剩下的就是素数”。
从 2 开始,每遇到一个素数就把它的所有倍数标记为合数。
bool is_composite[MAXN];
vector<int> primes;
void sieve(int n) {
for (int i = 2; i <= n; i++) {
if (!is_composite[i]) primes.push_back(i);
for (int j = 0; j < primes.size() && (long long)i * primes[j] <= n; j++) {
is_composite[i * primes[j]] = true;
if (i % primes[j] == 0) break;
}
}
}
这是线性筛(欧拉筛):每个合数只被它的最小素因子筛一次,总时间 。
if (i % primes[j] == 0) break 是关键——当 是 primes[j] 的倍数时,primes[j] 就是 的最小素因子,后续更大的素数与 的乘积会被更小的素因子先筛掉,所以可以 break。
2.3 素性测试
对单个大数 判断是否为素数:
- 试除法:枚举到 ,
- Miller-Rabin:随机化算法,, 轮后错误概率
竞赛中, 时,用一组特定的基 即可 100% 正确判定。
三、欧拉函数
3.1 定义
: 到 中与 互素的正整数个数。
其中 遍历 的所有不同素因子。
例如 。验证:,确实 4 个。
3.2 积性
若 ,则 。
这直接来自定义—— 和 的素因子集不相交,两个乘积直接合并。所以只需知道素数幂的欧拉函数值:
直觉: 到 中与 不互素的数恰好是 的倍数,共 个。
3.3 欧拉定理
若 ,则 。
费马小定理是它在 为素数时的特例()。
意义:费马小定理只对素数模数有效,但实际题目中模数不一定是素数。欧拉定理给出了更一般的周期——只要 和 互素, 的幂次每 步回到 1。
意义:提供了模非素数的指数周期。当模数不是素数但与 互素时,用 代替 即可。
3.4 用线性筛求欧拉函数
int phi[MAXN];
bool is_composite[MAXN];
vector<int> primes;
void phi_sieve(int n) {
phi[1] = 1;
for (int i = 2; i <= n; i++) {
if (!is_composite[i]) {
primes.push_back(i);
phi[i] = i - 1; // 素数:φ(p) = p - 1
}
for (int j = 0; j < primes.size() && (long long)i * primes[j] <= n; j++) {
is_composite[i * primes[j]] = true;
if (i % primes[j] == 0) {
// primes[j] | i,最小素因子的幂次增加
phi[i * primes[j]] = phi[i] * primes[j];
break;
} else {
// 积性
phi[i * primes[j]] = phi[i] * (primes[j] - 1);
}
}
}
}
当 primes[j] | i 时, 与 有完全相同的素因子集,只是 的幂次多了 1。由 的递推关系,。
当 primes[j] ∤ i 时,,由积性直接 。
四、求约数个数与约数和
对 :
| 量 | 公式 | 直觉 |
|---|---|---|
| 约数个数 | 每个素因子的幂次选 到 | |
| 约数和 | 每个素因子贡献等比数列之和 |
也可以在筛的过程中同时求出,利用积性维护每个数的最小素因子的幂次。
五、实战:求
给定 (),求 。
朴素 可以过,但有个 的数论做法。
把求和按 的值分类:
,所以满足条件的 恰好有 个:
枚举 的所有约数 ,对每个约数查 ,累加即可。
#include <cstdio>
using namespace std;
const int MAXN = 1000006;
int phi[MAXN];
bool is_comp[MAXN];
int primes[80000], pcnt;
void sieve(int n) {
phi[1] = 1;
for (int i = 2; i <= n; i++) {
if (!is_comp[i]) { primes[pcnt++] = i; phi[i] = i - 1; }
for (int j = 0; j < pcnt && (long long)i * primes[j] <= n; j++) {
is_comp[i * primes[j]] = true;
if (i % primes[j] == 0) { phi[i * primes[j]] = phi[i] * primes[j]; break; }
phi[i * primes[j]] = phi[i] * (primes[j] - 1);
}
}
}
int main() {
int n;
scanf("%d", &n);
sieve(n);
long long ans = 0;
for (int d = 1; (long long)d * d <= n; d++) {
if (n % d == 0) {
ans += (long long)d * phi[n / d];
if (d != n / d)
ans += (long long)(n / d) * phi[d];
}
}
printf("%lld\n", ans);
return 0;
}
解析:核心是把”求和”转化为”按 值分类”,然后发现每一类的计数就是 。枚举约数只需 (对每个 检查 和 ),配合 的欧拉函数预处理,总体效率远优于逐项计算。