素数判定是 ACM 最基础的操作之一。根据数据规模不同,用错方法可能从「秒过」变「TLE 到死」。本文从试除法开始,到筛法,再到 Miller-Rabin,讲清楚每种方法能处理多大范围、什么时候用。
一、试除法
1.1 朴素版
从 2 到 n-1 逐个试除,找到一个因子就是合数。
bool is_prime(int n) { if (n < 2) return false; for (int i = 2; i < n; i++) if (n % i == 0) return false; return true;}能处理的范围:(勉强)
1.2 优化:只除到 √n
若 n 是合数,必有一个因子 。
bool is_prime(int n) { if (n < 2) return false; for (int i = 2; i * i <= n; i++) // 只到 √n if (n % i == 0) return false; return true;}能处理的范围:(约需 次运算,能跑); 时会超时。
1.3 毫秒级优化:6k ± 1 跳跃
所有 ≥ 5 的素数都是 的形式。
bool is_prime(int n) { if (n < 2) return false; if (n == 2 || n == 3) return true; if (n % 2 == 0 || n % 3 == 0) return false;
for (int i = 5; i * i <= n; i += 6) { if (n % i == 0 || n % (i + 2) == 0) return false; } return true;}跳过了 2 和 3 的倍数,减少约 的工作量。常数优化,复杂度不变。
试除法小结
| 版本 | 范围 | 备注 |
|---|---|---|
| 朴素 | 别用 | |
| 到 √n | ACM 最常用 | |
| 6k±1 | 常数更快 | |
| 不能处理 | 换 Miller-Rabin |
二、埃氏筛与欧拉线性筛
当需要批量生成素数(1~N 全部找出来)时,筛法比反复调 is_prime() 快得多。
2.1 埃拉托斯特尼筛法(埃氏筛)
素数的倍数一定是合数。从最小的素数开始,把它的倍数标记掉;下一个没被标记的数就是素数。
vector<int> eratosthenes(int n) { vector<bool> is_prime(n + 1, true); is_prime[0] = is_prime[1] = false;
for (int i = 2; i * i <= n; i++) { if (is_prime[i]) { for (int j = i * i; j <= n; j += i) { is_prime[j] = false; } } }
vector<int> primes; for (int i = 2; i <= n; i++) if (is_prime[i]) primes.push_back(i); return primes;}关键点:
- 外层只到 (为什么?合数必有一个因子 )
- 内层从 开始(为什么? 早被更小的素数筛过了)
复杂度:,实际非常接近线性。
2.2 常数优化:跳过偶数
vector<int> eratosthenes_fast(int n) { vector<bool> is_prime(n + 1, true); is_prime[0] = is_prime[1] = false;
for (int i = 4; i <= n; i += 2) is_prime[i] = false; // 偶数全标记
for (int i = 3; i * i <= n; i += 2) { // 只检查奇数 if (is_prime[i]) for (int j = i * i; j <= n; j += 2 * i) is_prime[j] = false; }
vector<int> primes; primes.push_back(2); for (int i = 3; i <= n; i += 2) if (is_prime[i]) primes.push_back(i); return primes;}就加几行,速度再提一个档次。
2.3 欧拉筛(线性筛)
埃氏筛的问题:同一个合数可能被多个质因子重复标记。
12 会被 2 标记一次,又被 3 标记一次 → 重复欧拉筛的核心:每个合数只被它的最小质因子(LPF)筛掉,真正 。
vector<int> euler_sieve(int n) { vector<bool> is_prime(n + 1, true); vector<int> primes;
is_prime[0] = is_prime[1] = false;
for (int i = 2; i <= n; i++) { if (is_prime[i]) primes.push_back(i);
for (int p : primes) { if (i * p > n) break; is_prime[i * p] = false; // 筛掉 i×p if (i % p == 0) break; // ★ 关键 } } return primes;}if (i % p == 0) break 是什么意思?
p是从小到大枚举的已知素数- 当
i % p == 0,说明p是i的最小质因子 - 如果继续用更大的 筛, 的最小质因子是 p 而不是 p’,违背了”只用最小质因子筛”的原则
- 所以要停,后面的留到更大的 i 去筛
额外收获:修改三行就能同时拿到每个数的最小质因子,之后 分解质因数:
vector<int> lpf(n + 1, 0);// ... 在筛的时候顺手填 lpf[i * p] = p;// 之后:while (x > 1) { int p = lpf[x], cnt = 0; while (x % p == 0) cnt++, x /= p; // (p, cnt) 就是一个质因子及其次数}筛法对比
| 埃氏筛 | 欧拉筛 | |
|---|---|---|
| 复杂度 | ||
| 重复标记 | 有 | 无 |
| 额外信息 | 仅 is_prime | 可同时拿 LPF |
| 代码量 | 5 行 | 10 行 |
| N=10⁷ 实测 | ~0.10s | ~0.15s |
| 何时用 | 日常刷题 | 需要质因数分解,或 N 极大 |
三、Miller-Rabin 素性测试
当 n 超过 ,试除法跑不动了。Miller-Rabin 能在毫秒内判定 64 位范围的任意整数是否为素数。
3.1 能处理多大?
| 方法 | 可判定范围 |
|---|---|
| 试除法 | |
| Miller-Rabin |
3.2 模板(直接复制用)
#include <bits/stdc++.h>using namespace std;#define int long long
int qpow(int a, int b, int mod) { int res = 1; a %= mod; while (b) { if (b & 1) res = (__int128)res * a % mod; a = (__int128)a * a % mod; b >>= 1; } return res;}
bool is_prime(int n) { if (n < 2) return false; if (n == 2) return true; if (n % 2 == 0) return false;
// 底数选择(这里用 7 个底数覆盖 64 位,放模板里不用记) int bases[] = {2, 325, 9375, 28178, 450775, 9780504, 1795265022};
int d = n - 1, s = 0; while (d % 2 == 0) { d /= 2; s++; }
for (int a : bases) { if (a % n == 0) continue;
int x = qpow(a, d, n); if (x == 1 || x == n - 1) continue;
for (int r = 1; r < s; r++) { x = (__int128)x * x % n; if (x == n - 1) break; } if (x != n - 1) return false; } return true;}3.3 底数记忆法
ACM 中 99% 的题 n ≤ 2³²(约 4.3×10⁹),只需要记 3 个底数:
{2, 7, 61}2 → 7 → 61,三个素数,连续变大。记住就行。
如果题目 n 大到 64 位,把模板里的 7 个底数写死在代码里,不用记。那一长串就是放在模板里存着的。
| n 的范围 | 底数 | 需要记吗 |
|---|---|---|
| < 2³²(常规题) | {2, 7, 61} | ✅ 记住 |
| < 2⁶⁴(极少数) | {2, 325, 9375, 28178, 450775, 9780504, 1795265022} | ❌ 放模板 |
3.4 怎么用?三步
① 复制模板代码到你开赛的 .cpp 里② 调用 is_prime(n),返回 true/false③ 完了cout << is_prime(998244353) << endl; // 1cout << is_prime(561) << endl; // 0(卡迈克尔数,试除法能过但 MR 能识破)3.5 如果编译器不支持 __int128
换用手写安全乘法:
int mul(int a, int b, int mod) { int res = 0; a %= mod; while (b) { if (b & 1) res = (res + a) % mod; a = (a + a) % mod; b >>= 1; } return res;}然后把 qpow 里的 (__int128)x * y % n 全换成 mul(x, y, n)。
3.6 常见坑
| 问题 | 解决 |
|---|---|
| 负数进来 | 开头 if (n < 2) return false; |
| 只判单个大数 | 用 MR;批量筛素数用埃氏筛 |
__int128 不支持 | 用手写 mul() |
| n 在 32 位以内 | 把底数改成 {2, 7, 61},少跑几轮更快 |
四、总览:什么时候用哪个
需要批量生成素数? ──── 是 ──→ 埃氏筛(或欧拉筛拿 LPF) │ 否 │ ↓ n ≤ 10¹²? ──── 是 ──→ 试除法(到 √n,6k±1 优化) │ 否 │ ↓ n ≤ 2⁶⁴? ──── 是 ──→ Miller-Rabin │ 否 │ ↓ 大数库 / Python 的 isprime()三件武器,按数据规模选。ACMer 背包里常备这三样就够了。
五、实战案例:Primer Distance(POJ 2689)
题目
给定多组 ,,。输出区间内相邻素数差的最大值和最小值。
核心难点
可以到 ,直接用埃氏筛 → 内存爆炸。但 很小——这是突破口。
两种思路
思路一:筛 的素数表,再用它们去筛区间 (分段筛)。
思路二:Miller-Rabin 遍历 每个数,逐个判定。
| 分段筛 | Miller-Rabin 硬扫 | |
|---|---|---|
| 代码量 | 多几行 | 直接套模板 |
| 速度 | 筛表 O(√r),区间 O((r-l) log log r) | 最坏 ~10⁶ × 7 轮模幂 |
| 稳妥性 | ✅ 所有 OJ 通吃 | ⚠️ 常数大,可能 TLE |
结论:分段筛是标准答案。
分段筛解法
① 筛 [2, √r] 的素数 → 不到 5000 个素数② 用这些素数标记 [l, r] 的合数 → 10⁶ 的 bool 数组③ 收集区间素数,遍历找答案 → O(区间长度)#include <bits/stdc++.h>using namespace std;
vector<int> get_primes(int n) { vector<bool> is_prime(n + 1, true); vector<int> primes; for (int i = 2; i <= n; i++) { if (is_prime[i]) primes.push_back(i); for (int j = 0; j < (int)primes.size() && i * primes[j] <= n; j++) { is_prime[i * primes[j]] = false; if (i % primes[j] == 0) break; } } return primes;}
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
// ① 只筛 √(2³¹) ≈ 46340 auto small = get_primes(50000);
long long l, r; while (cin >> l >> r) { // ② 用小区间标记合数 vector<bool> is_prime(r - l + 1, true); for (long long p : small) { if (p * p > r) break; // 找到 p 在 [l, r] 内的第一个倍数 long long start = max(p * p, (l + p - 1) / p * p); for (long long j = start; j <= r; j += p) is_prime[j - l] = false; } if (l == 1) is_prime[0] = false; // 1 不是素数
// ③ 收集区间内的素数 vector<long long> primes; for (long long i = l; i <= r; i++) if (is_prime[i - l]) primes.push_back(i);
if (primes.size() < 2) { cout << "There are no adjacent primes.\n"; continue; }
long long mn = primes[1] - primes[0], mx = mn; long long c1 = primes[0], c2 = primes[1]; // closest long long d1 = primes[0], d2 = primes[1]; // distant
for (int i = 1; i < (int)primes.size() - 1; i++) { long long diff = primes[i + 1] - primes[i]; if (diff < mn) mn = diff, c1 = primes[i], c2 = primes[i + 1]; if (diff > mx) mx = diff, d1 = primes[i], d2 = primes[i + 1]; }
printf("%lld,%lld are closest, %lld,%lld are most distant.\n", c1, c2, d1, d2); }}关键细节
① 起始位置的写法
long long start = max(p * p, (l + p - 1) / p * p);p * p:从 p² 开始,跟埃氏筛一样——更小的倍数早被更小的素数筛过了(l + p - 1) / p * p:如果 ,需要找到 p 在区间内的第一个倍数。(l + p - 1) / p是向上取整除法
例:l=100, p=7 (100 + 7 - 1) / 7 * 7 = 106 / 7 * 7 = 15 * 7 = 105 105 是 ≥100 的第一个 7 的倍数 ✓② 为什么筛到 50000 而不是 46340?
46340 是 的精确值,取 50000 多筛一点不碍事,写代码方便。
③ 数组偏移 j - l
is_prime[j - l] = false;区间起始是 l,不是 1。用 j - l 映射到数组下标。数组大小 r - l + 1。
你的代码回顾
回到你之前的那段代码,问题在于:
| 你的写法 | 问题 |
|---|---|
n = pow(2, 30) | 筛了 10 亿个,~560MB 内存,必爆 MLE |
is_prime[0],[1] 未置 false | 0 和 1 会被当成素数 |
it2 = upper_bound(u) - 1 直接用 | 没有先判断区间里有没有素数,可能越界 |
maxn = -1 / minn = 999... | 魔法数字,不优雅 |
思路没方向性错误,只是筛的范围搞错了——不该筛整个 [0, 2^30],只筛 √r,然后分段筛区间,正是这题的核心技巧。
六、阶乘分解(P10495)
题目
给定 ,将 分解质因数,按算术基本定理输出每个质因子及其次数。
输入:5输出:2 3 (5! = 2³ × 3¹ × 5¹) 3 1 5 1思路对比
| 朴素:分解每个数 | 勒让德公式 | |
|---|---|---|
| 做法 | 遍历 2→N,每个数质因数分解,累加到桶 | 对每个素数 p,直接算它在 N! 中的总次数 |
| 运算量 | O(N log N),常数大 | 约 N 个素数 × log_p(N) 次除法 |
| 需要桶 | 需要 | 不需要,算一个输出一个 |
勒让德公式:
为什么?
中:
- 每 个数里有一个是 的倍数,贡献 1 个 →
- 每 个数里有一个额外再贡献 1 个 →
- 每 个同理 →
N = 5, p = 2 时: 5! = 1 × 2 × 3 × 4 × 5 ↑ ↑ ⌊5/2⌋ = 2(2 和 4) ⌊5/4⌋ = 1(4 多贡献一个) → 2 + 1 = 3,5! 含 2³ ✓代码
#include <bits/stdc++.h>using namespace std;
int main() { int N; cin >> N;
// 筛 ≤ N 的素数 bitset<1000005> is_prime; is_prime.set(); is_prime[0] = is_prime[1] = 0; for (int i = 2; i * i <= N; i++) if (is_prime[i]) for (int j = i * i; j <= N; j += i) is_prime[j] = 0;
// 勒让德公式:对每个素数直接算次数 for (int p = 2; p <= N; p++) { if (!is_prime[p]) continue;
int cnt = 0; long long power = p; while (power <= N) { cnt += N / power; power *= p; } cout << p << " " << cnt << "\n"; }}本质
朴素法是「先乘出阶乘,再拆开」——绕了远路。勒让德公式直接在阶乘层面统计每个质因子的贡献次数,完全跳过了对每个数的分解。
总结
| 场景 | 武器 |
|---|---|
| 判别单个数(n ≤ 10¹²) | 试除法到 √n |
| 判别单个大数(n ≤ 2⁶⁴) | Miller-Rabin |
| 批量生成 [1, N] 素数 | 埃氏筛 / 欧拉筛 |
| 大区间、小区间长度 | 分段筛(筛 √r → 筛区间) |
| N! 质因数分解 | 勒让德公式(筛素数表 + 指数求和) |
五种武器,覆盖所有素数相关场景。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时
