什么是线性筛

线性筛是一种在 O(N)O(N)O(N) 时间复杂度内,筛出 111NNN 中所有质数的算法。它同时也是预处理最小质因子、欧拉函数、约数个数等数论函数的强有力框架。

相比朴素的埃氏筛(埃拉托斯特尼筛法)O(Nlog⁡log⁡N)O(N \log \log N)O(NloglogN) 的复杂度,线性筛将复杂度压到了真正的线性,代价是代码稍复杂一些,但换来的能力远超单纯的质数筛。


埃氏筛的问题

埃氏筛的思想很简单:从小到大遍历,遇到一个质数,就把它的所有倍数标记为合数。

for (int i = 2; i <= n; ++i) {
    if (!is_comp[i]) {
        primes.push_back(i);
        for (int j = i * 2; j <= n; j += i)
            is_comp[j] = true;
    }
}

问题在于,一个合数可能被多个质数重复标记。例如 121212,会被质数 222 标记一次,又会被质数 333 再标记一次。这种重复操作使得复杂度无法达到线性。


线性筛的核心思想

线性筛的目标是:每个合数,只被它的最小质因子筛掉一次。

设合数 xxx 的最小质因子为 ppp,则 x=i×px = i \times px=i×p。我们希望在枚举 iii 时,用质数 ppp 恰好筛掉 xxx,此后不再用其他质数去碰它。

这就需要一个关键判断:当 ppp 能整除 iii 时,立即停止用更大的质数去筛 i×p′i \times p'i×p,因为这些合数的最小质因子已经不是 p′p'p,而是 ppp 了。


代码实现与逐行解释

const int MAXN = 1e6 + 5;
int primes[MAXN], cnt;      // 收集质数
bool is_comp[MAXN];         // 是否为合数
int minp[MAXN];             // 最小质因子

void linear_sieve(int n) {
    for (int i = 2; i <= n; ++i) {
        // 如果 i 还没有被标记为合数,它就是质数
        if (!is_comp[i]) {
            primes[cnt++] = i;
            minp[i] = i;          // 质数的最小质因子是它自己
        }

        // 用已有的质数去筛合数
        for (int j = 0; j < cnt && i * primes[j] <= n; ++j) {
            int p = primes[j];
            is_comp[i * p] = true;    // i * p 一定是合数
            minp[i * p] = p;          // p 是 i*p 的最小质因子

            if (i % p == 0) break;    // 关键:一旦 p | i,立刻停止
        }
    }
}

if (i % p == 0) break; 为什么关键?

我们手动模拟 n=6n=6n=6 的过程,聚焦这行代码:

  • i = 2:质数,primes=[2]。内循环 p=2,筛掉 444i % 2 == 0,break。
  • i = 3:质数,primes=[2,3]。内循环 p=2,筛掉 6663%2 != 0,继续;p=3,筛掉 999;break。
  • i = 4:合数,minp[4]=2。内循环 p=2,筛掉 8884%2==0,break。注意:4 不会和 p=3 相乘,因此 121212 不会在这里被筛。
  • i = 5:质数,筛掉 10,1510,1510,15
  • i = 6:合数,minp[6]=2。内循环 p=2,筛掉 1212126%2==0,break。

可以看到,121212i=6, p=2 筛掉,而不是被 i=4, p=3 筛掉。这正是因为我们每次都在 i % p == 0 时 break,保证了筛掉每个合数所用的质数,恰好是该合数的最小质因子。

推广到一般情况:对于当前 iii,设其最小质因子为 p0p_0p0。当我们枚举质数 ppp 去乘 iii 时:

  • p<p0p < p_0p<p0,则 pppi×pi \times pi×p 的最小质因子,合理筛掉。
  • p=p0p = p_0p=p0,即 i % p == 0ppp 恰好是 i×pi \times pi×p 的最小质因子,筛掉后应立即停止。因为下一个更大的质数 p′p'piii 所得的 i×p′i \times p'i×p,它的最小质因子是 p0p_0p0 而不是 p′p'p,应该留给后续更大的 i′i'ip0p_0p0 去筛。

这样,每个合数都只会进入内循环一次,复杂度严格 O(N)O(N)O(N)


线性筛的扩展能力

线性筛的威力远不止找出质数。在筛的过程中,我们可以顺便求出最小质因子欧拉函数约数个数等信息,因为它们都满足积性函数的递推性质。

1. 最小质因子

上文的 minp 数组已经给出。minp[i] 记录了 iii 的最小质因子。这使得后续单个数质因数分解的复杂度降为 O(log⁡N)O(\log N)O(logN)

void factorize(int x) {
    while (x > 1) {
        int p = minp[x];
        int cnt = 0;
        while (x % p == 0) x /= p, cnt++;
        // 输出或存储 (p, cnt)
    }
}

2. 欧拉函数 φ(n)\varphi(n)φ(n)

欧拉函数 φ(n)\varphi(n)φ(n) 表示 111nnn 中与 nnn 互质的数的个数。它满足:

  • φ(p)=p−1\varphi(p) = p - 1φ(p)=p1ppp 是质数)
  • p∣ip \mid ipi,则 φ(i×p)=φ(i)×p\varphi(i \times p) = \varphi(i) \times pφ(i×p)=φ(i)×p
  • p∤ip \nmid ipi,则 φ(i×p)=φ(i)×(p−1)\varphi(i \times p) = \varphi(i) \times (p - 1)φ(i×p)=φ(i)×(p1)

我们可以把它整合到线性筛中:

int phi[MAXN];

void sieve_with_phi(int n) {
    phi[1] = 1;
    for (int i = 2; i <= n; ++i) {
        if (!is_comp[i]) {
            primes[cnt++] = i;
            phi[i] = i - 1;
        }
        for (int j = 0; j < cnt && i * primes[j] <= n; ++j) {
            int p = primes[j];
            is_comp[i * p] = true;
            if (i % p == 0) {
                phi[i * p] = phi[i] * p;
                break;
            }
            phi[i * p] = phi[i] * (p - 1);
        }
    }
}

3. 约数个数

d(n)d(n)d(n)nnn 的约数个数,a(n)a(n)a(n)nnn 的最小质因子的指数。在线性筛中同样可以维护:

  • nnn 为质数:d(n)=2d(n)=2d(n)=2a(n)=1a(n)=1a(n)=1
  • p∤ip \nmid ipi,则 d(i×p)=d(i)×2d(i \times p) = d(i) \times 2d(i×p)=d(i)×2a(i×p)=1a(i \times p) = 1a(i×p)=1
  • p∣ip \mid ipi,则 d(i×p)=d(i)/(a(i)+1)×(a(i)+2)d(i \times p) = d(i) / (a(i)+1) \times (a(i)+2)d(i×p)=d(i)/(a(i)+1)×(a(i)+2)a(i×p)=a(i)+1a(i \times p) = a(i) + 1a(i×p)=a(i)+1

代码类似,不再赘述。


线性筛 vs 埃氏筛

埃氏筛 线性筛
时间复杂度 O(Nlog⁡log⁡N)O(N \log \log N)O(NloglogN) O(N)O(N)O(N)
空间复杂度 O(N)O(N)O(N) O(N)O(N)O(N)
是否重复标记合数
能否同时求最小质因子 需要额外处理 可以直接整合
能否递推欧拉函数等 不方便 非常方便
代码复杂度 简单 略复杂

N≤107N \le 10^7N107 时,埃氏筛的实际速度与线性筛相差不大。但当需要维护最小质因子、欧拉函数等附加信息时,线性筛是首选框架。


总结

线性筛通过“每个合数只被最小质因子筛掉一次”的核心机制,将筛法复杂度压到 O(N)O(N)O(N),并提供了一个可以嵌入多种积性函数递推的框架。理解 if(i % p == 0) break; 这句话,就理解了线性筛的全部精髓。

对于算法竞赛,线性筛是处理 10710^7107 范围内数论预处理的标准工具,也是后续质因数分解、互质计数、狄利克雷卷积等问题的基础。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐