从TLE到AC:3种素数筛法的极限优化与实战对比
你是否还在为素数筛选超时发愁?当数据规模突破千万级,传统埃氏筛法(Sieve of Eratosthenes)的效率瓶颈如何突破?本文将系统解析埃氏筛、欧拉筛(Euler Sieve)与区间筛的实现原理,通过12个优化点将时间复杂度从O(n log log n)降至O(n),并提供可直接运行的代码模板。读完本文你将掌握:
- 埃氏筛的4种关键优化(平方根截断/位压缩/分块筛选等)
- 欧拉筛避免重复标记的核心机制(最小质因子分解)
- 区间筛处理1e12级大整数的实现方案
- 3种筛法在不同数据规模下的性能对比与选型策略
埃氏筛法:基础原理与优化实践
埃拉托斯特尼筛法(Eratosthenes Sieve)通过迭代标记每个素数的倍数来找出一定范围内的所有素数。其基本实现如docs/math/number-theory/sieve.md所示:
vector<int> prime;
bool is_prime[N];
void Eratosthenes(int n) {
is_prime[0] = is_prime[1] = false;
for (int i = 2; i <= n; ++i) is_prime[i] = true;
for (int i = 2; i <= n; ++i) {
if (is_prime[i]) {
prime.push_back(i);
if ((long long)i * i > n) continue;
for (int j = i * i; j <= n; j += i)
is_prime[j] = false; // 标记i的倍数为合数
}
}
}
关键优化技术
1. 平方根截断优化
观察可知只需筛选到√n即可,因为大于√n的合数必定有小于√n的因子。优化后外层循环次数从n降至√n:
for (int i = 2; i * i <= n; ++i) { // i循环到sqrt(n)
if (is_prime[i])
for (int j = i * i; j <= n; j += i) is_prime[j] = false;
}
2. 位级压缩存储
使用bitset或vector
将内存占用减少8倍,同时提升缓存命中率。如
docs/lang/csl/bitset.md#与埃氏筛结合所述,bitset优化能使埃氏筛性能超越理论O(n)的欧拉筛:
bitset<100000001> is_prime; // 仅占用12.5MB内存
void Eratosthenes_bitset(int n) {
is_prime.set(); // 全部置1
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;
}
3. 分块筛选策略
将大区间分成小块处理(典型块大小1e4-1e5),只需保留√n以内的素数表,内存占用降至O(√n + S)。实现代码见docs/math/number-theory/sieve.md分块筛法部分,该方法特别适合处理超过内存限制的大素数筛选任务。
欧拉筛法:线性时间的极致追求
欧拉筛通过保证每个合数仅被其最小质因子标记,实现了O(n)的线性时间复杂度。核心机制在于当i % pri_j == 0时终止循环,避免重复标记:
vector<int> pri;
bool not_prime[N];
void euler_sieve(int n) {
for (int i = 2; i <= n; ++i) {
if (!not_prime[i]) pri.push_back(i); // i是素数
for (int pri_j : pri) {
if (i * pri_j > n) break;
not_prime[i * pri_j] = true;
if (i % pri_j == 0) break; // 保证pri_j是最小质因子
}
}
}
算法正确性证明
对于任意合数x,设其最小质因子为p,则x可表示为p * k。当i=k时:
- 若k < p,则p会在后续被加入pri数组,此时i=k < p,循环会在i*p > n前标记x
- 若k >= p,则k中必包含p作为因子,此时i%p == 0,循环终止前已标记x
因此每个合数恰被标记一次,时间复杂度严格为O(n)。
区间筛法:超大素数的筛选方案
当需要筛选[L, R]区间内的素数(如R达1e12)时,区间筛法通过以下步骤实现:
- 用埃氏筛生成√R以内的素数表
- 对每个素数p,标记区间内p的倍数
- 未被标记的数即为区间素数
核心实现代码如下(完整版本见docs/math/number-theory/sieve.md):
vector<int> get_primes(int n) { // 生成√R以内素数
vector<int> primes;
vector<bool> is_prime(n+1, true);
// ... 埃氏筛实现 ...
return primes;
}
int count_primes_in_range(int L, int R) {
vector<int> primes = get_primes(sqrt(R));
vector<bool> block(R-L+1, true);
for (int p : primes) {
int start = max(p*p, (L+p-1)/p*p); // 计算区间内第一个p的倍数
for (int j = start; j <= R; j += p)
block[j-L] = false;
}
// 处理L=0和L=1的特殊情况
return count(block.begin(), block.end(), true);
}
性能对比与实战选型
| 筛法类型 | 时间复杂度 | 内存占用 | 适用场景 | 千万级数据耗时 |
|---|---|---|---|---|
| 基础埃氏筛 | O(n log log n) | O(n) | 中小规模素数筛选 | ~80ms |
| 位优化埃氏筛 | O(n log log n) | O(n/8) | 内存受限场景 | ~35ms |
| 欧拉筛 | O(n) | O(n) | 线性时间需求场景 | ~45ms |
| 区间筛 | O((R-L) log log √R + √R log log √R) | O(R-L + √R) | 超大范围素数筛选 | 视区间大小而定 |
优化建议
- 数据量≤1e7:优先选择位优化埃氏筛(缓存友好)
- 数据量>1e7且内存充足:欧拉筛更优
- 数据量>内存限制:分块筛或区间筛
- 多组测试用例:预处理素数表+前缀和
工程实现注意事项
- 数组大小声明:全局数组可声明更大空间,局部数组需注意栈溢出
- 类型溢出处理:计算i*pri_j时需强制转换为long long避免溢出
- 缓存优化:分块筛法中块大小设为CPU缓存大小的整数倍可提升性能
- 并行优化:大规模筛选可采用OpenMP并行标记(需注意线程安全)
完整代码实现与更多优化细节,请参考docs/math/number-theory/sieve.md及docs/math/number-theory/prime.md相关章节。掌握这些素数筛选技术,将为数论问题求解奠定坚实基础。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



