Skip to content

埃氏筛与线性筛:两种筛法详解

基于通用数论套路 · 核于 2026-07

速查

  • 埃氏筛思路:从 2 起,每遇到一个仍标记为素数的 p,就把 p 的所有倍数 2p、3p、... 标记为合数——素数「划掉」自己的倍数。
  • 埃氏筛复杂度O(n log log n)(调和级数 ∑ 1/p,p 取素数,增长极慢,n=10⁷ 时约 3,几乎线性)。
  • 埃氏筛的重复问题:一个合数会被它的所有素因子各筛一次——如 12212=2×6)和 312=3×4)各筛一次,302、3、5 筛三次。
  • 线性筛核心约束:让每个合数只被它的最小质因子筛掉一次——通过 i % p === 0break 实现。
  • 线性筛的 break 为何有效:设 i = p × kpi 的最小质因子),对更大的素数 q > p,合数 i × q = p × (k × q) 的最小质因子仍是 p 而非 q,应留给「外层 = k×q、内层素数 = p」那次来筛,故此时必须 break
  • 线性筛复杂度严格 O(n)——每个合数只被访问一次;空间额外 O(π(n)) 存素数表(π(n) 为 ≤n 的素数个数,约 n/ln n)。
  • 关键代码差异:埃氏筛内层 for (k = 2p; k ≤ n; k += p);线性筛内层遍历素数表且 if (i % p === 0) break
  • 外层边界:埃氏筛外层只需到 √n(更大的素数 pp² > n,无 ≤n 的倍数可筛);线性筛外层必须到 n
  • 空间:两者均需 O(n) 的标记数组;线性筛额外 O(n/ln n) 的素数表。
  • 易错:内层起点 2p 而非 pp 自己是素数不能删);线性筛漏 break 会退化且重复;isPrime[0]=isPrime[1]=false 必须初始化。
  • 选型n ≤ 10⁷ 埃氏筛常数小通常更快;n 更大或理论分析要求严格线性时用线性筛。

一、埃氏筛:素数划掉自己的倍数

埃拉托斯特尼筛(Sieve of Eratosthenes)是最古老也最直观的筛法。思路一句话:每个素数 p 把它的所有倍数标成合数。因为合数 k 一定有某个素因子 p,当外层遍历到 p 时,k(作为 p 的倍数)必然被标记——所以遍历结束后,仍为 true 的就是素数。

js
function eratosthenes(n) {
  const isPrime = new Array(n + 1).fill(true);
  isPrime[0] = isPrime[1] = false;          // 0、1 不是素数
  for (let p = 2; p * p <= n; p++) {        // 外层只需到 √n
    if (!isPrime[p]) continue;              // p 是合数(已被更小素数筛掉),跳过
    for (let k = p * p; k <= n; k += p)     // 从 p² 开始筛(更小倍数已筛)
      isPrime[k] = false;
  }
  return isPrime;
}

注意两处优化:①外层只到 √np > √np² > n,没有 ≤n 的倍数可筛);②内层从 开始而非 2p2p、3p、...(p-1)p 这些倍数在筛更小素数时已经标过了)。

复杂度:O(n log log n)

总标记次数是 ∑ₚ n/p(p 取素数)= n × (1/2 + 1/3 + 1/5 + 1/7 + ...)。素数倒数和约等于 ln ln n + M(M 为 Meissel-Mertens 常数),所以总次数约 n ln ln n,即 O(n log log n)log log n 增长极慢:

nlog log n(约)
10⁶2.6
10⁹3.0

所以实际中埃氏筛「几乎线性」,常数又小,是绝大多数场景的首选。

二、埃氏筛为什么有重复

埃氏筛的代价是一个合数会被多次标记。设合数 m = p₁ × p₂ × ...,则对它的每一个素因子 pᵢ,外层遍历到 pᵢ 时都会把 m(作为 pᵢ 的倍数)标一次。以 12 为例:

m = 12 = 2² × 3
外层到 p=2:标 4, 6, 8, 10, 12, ...  → 12 被标第 1 次
外层到 p=3:标 9, 12, 15, ...        → 12 被标第 2 次

12 被素因子 23 各筛一次。倍数素因子越多,重复越严重——30 = 2×3×5 被筛三次。这些重复标记虽然不影响正确性(多标几次 false 还是 false),但拉高了常数项,使复杂度从 O(n) 退化到 O(n log log n)。线性筛的目标就是消除这些重复。

三、线性筛:每个合数只筛一次

线性筛(又称欧拉筛)通过一个精巧的约束——每个合数只被它的最小质因子筛掉——保证每个合数恰好被标记一次,从而达成严格 O(n)。它额外维护「已知素数表 primes」,对外层数 i(从 2 到 n),遍历 primes 里的每个素数 p,筛掉合数 i × p一旦 i 能被 p 整除就 break

js
function linearSieve(n) {
  const isPrime = new Array(n + 1).fill(true);
  const primes = [];
  isPrime[0] = isPrime[1] = false;
  for (let i = 2; i <= n; i++) {
    if (isPrime[i]) primes.push(i);        // i 是素数,登记入表
    for (let j = 0; j < primes.length; j++) {
      const p = primes[j];
      if (p * i > n) break;                // 合数超出范围
      isPrime[p * i] = false;              // 筛掉 i × p
      if (i % p === 0) break;              // 关键:保证只被最小质因子筛
    }
  }
  return primes;
}

为何 i % p === 0 时必须 break

这是线性筛正确性的核心。设 i 能被 p 整除,即 i = p × k(此时 pi 的最小质因子)。考虑素数表里 p 之后的更大素数 q > p,若不 break 继续筛 i × q

i × q = p × k × q

这个合数的最小质因子是 p(因为 p < qp 整除它),不是 q。按「只被最小质因子筛」的约定,它应该留给「外层 i' = k × q、内层素数 p」那一次来筛(届时 i' × p = k × q × p = i × q)。如果现在用 q 筛了它,等 i' = k × q 时又会用 p 再筛一次——重复!

所以 break 的本质是:「既然 p 已经是 i 的因子,那么 i × (比 p 大的素数) 的最小质因子都是 p 而非那个大素数,应统一留给 p 来筛。」 这保证每个合数只在「最小质因子 × 某个数」的形式下被访问一次。

复杂度:严格 O(n)

由于每个合数 m 只在其 m = (最小质因子) × i 的唯一形式下被标记一次,内层总标记次数恰为 n 量级;外层 i 从 2 到 n。总体 O(n)。代价是空间多了素数表(O(n/ln n)),且常数(素数表访问、break 判断)大于埃氏筛——所以小数据下埃氏筛往往更快,线性筛的优势在 n 极大或理论分析时才显现。

四、两种筛法对照

维度埃氏筛线性筛(欧拉筛)
思路素数 pp 的倍数每个合数只被最小质因子筛
复杂度O(n log log n)O(n)
是否重复有(每个素因子各筛一次)无(每个合数筛一次)
外层范围√n 即可必须到 n
内层起点(优化)或 2pp × i(遍历素数表)
关键判断i % p === 0break
空间O(n)(标记数组)O(n) + O(n/ln n)(素数表)
常数小(更快)大(额外素数表与判断)
适用通用首选、n ≤ 10⁷严格线性、n 极大或积性函数筛

记忆口诀:「埃氏筛:素数筛倍数,简单但重复;线性筛:最小质因子筛一次,严格 O(n) 但常数大。」

交互演示

下一步

掌握两种筛法后,下一步看优化技巧与实际应用——从 起筛省一半常数、用筛法预处理的素数表做 O(1) 查询、用最小质因子表 spf 做 O(log n) 质因数分解,以及何时该选哪种筛,见优化与应用场景