线性筛(欧拉筛):从原理到应用

什么是线性筛

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

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


埃氏筛的问题

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

cpp 复制代码
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 了。


代码实现与逐行解释

cpp 复制代码
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,筛掉 444。i % 2 == 0,break。
  • i = 3 :质数,primes=[2,3]。内循环 p=2,筛掉 666;3%2 != 0,继续;p=3,筛掉 999;break。
  • i = 4 :合数,minp[4]=2。内循环 p=2,筛掉 888;4%2==0,break。注意:4 不会和 p=3 相乘 ,因此 121212 不会在这里被筛。
  • i = 5 :质数,筛掉 10,1510,1510,15。
  • i = 6 :合数,minp[6]=2。内循环 p=2,筛掉 121212;6%2==0,break。

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

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

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

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


线性筛的扩展能力

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

1. 最小质因子

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

cpp 复制代码
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) 表示 111 到 nnn 中与 nnn 互质的数的个数。它满足:

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

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

cpp 复制代码
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)=2,a(n)=1a(n)=1a(n)=1。
  • 若 p∤ip \nmid ip∤i,则 d(i×p)=d(i)×2d(i \times p) = d(i) \times 2d(i×p)=d(i)×2,a(i×p)=1a(i \times p) = 1a(i×p)=1。
  • 若 p∣ip \mid ip∣i,则 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^7N≤107 时,埃氏筛的实际速度与线性筛相差不大。但当需要维护最小质因子、欧拉函数等附加信息时,线性筛是首选框架。


总结

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

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

相关推荐
ysu_03141 小时前
leetcode数据结构与算法5~7:链表双指针与二级指针
数据结构·学习·算法·leetcode·链表
小欣加油1 小时前
leetcode542 01矩阵
数据结构·c++·算法·leetcode·矩阵·bfs
wu_ye_m1 小时前
学习c语言第34天 用函数每次输出+1,链式访问,int和void
c语言·学习·算法
星马梦缘1 小时前
算法设计与分析 作业三 答案与解析
算法·线性规划·二分图匹配·多元最短路·流网络·bellmanford·匈牙利树算法
微风欲寻竹影1 小时前
Java数据结构——二叉树(Binary Tree)详解
java·数据结构·算法
想吃火锅10051 小时前
【leetcode】3.无重复字符的最长字串js版
算法·leetcode·职场和发展
smith成长之旅1 小时前
08 | Mem0 框架分析: BM25 的 Sigmoid 归一化
数据库·python·算法
dongf20191 小时前
R 语言随机森林算法
算法·随机森林·r语言
AZaLEan__2 小时前
图论:拓扑排序
算法·深度优先