从TLE到AC:3种素数筛法的极限优化与实战对比

从TLE到AC:3种素数筛法的极限优化与实战对比

【免费下载链接】OI-wiki :star2: Wiki of OI / ICPC for everyone. (某大型游戏线上攻略,内含炫酷算术魔法) 【免费下载链接】OI-wiki 项目地址: https://gitcode.com/GitHub_Trending/oi/OI-wiki

你是否还在为素数筛选超时发愁?当数据规模突破千万级,传统埃氏筛法(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)时,区间筛法通过以下步骤实现:

  1. 用埃氏筛生成√R以内的素数表
  2. 对每个素数p,标记区间内p的倍数
  3. 未被标记的数即为区间素数

核心实现代码如下(完整版本见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且内存充足:欧拉筛更优
  • 数据量>内存限制:分块筛或区间筛
  • 多组测试用例:预处理素数表+前缀和

工程实现注意事项

  1. 数组大小声明:全局数组可声明更大空间,局部数组需注意栈溢出
  2. 类型溢出处理:计算i*pri_j时需强制转换为long long避免溢出
  3. 缓存优化:分块筛法中块大小设为CPU缓存大小的整数倍可提升性能
  4. 并行优化:大规模筛选可采用OpenMP并行标记(需注意线程安全)

完整代码实现与更多优化细节,请参考docs/math/number-theory/sieve.mddocs/math/number-theory/prime.md相关章节。掌握这些素数筛选技术,将为数论问题求解奠定坚实基础。

【免费下载链接】OI-wiki :star2: Wiki of OI / ICPC for everyone. (某大型游戏线上攻略,内含炫酷算术魔法) 【免费下载链接】OI-wiki 项目地址: https://gitcode.com/GitHub_Trending/oi/OI-wiki

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值