引言:为什么要用线性筛?

在之前的学习中,我们掌握了求单个数欧拉函数 φ(n)\varphi(n) 的方法,其时间复杂度是 O(n)O(\sqrt{n})

但在算法竞赛中,我们经常面临这样的需求:11NN(例如 10710^7)之间所有整数的欧拉函数值。

如果我们对每个数都单独计算,总复杂度将高达 O(NN)O(N\sqrt{N})。对于 N=107N=10^7,这意味着 101010^{10} 次运算,绝对会 TLE(超时)。

这时候,我们需要引入线性筛(欧拉筛)。它不仅能以 O(N)O(N) 的线性时间复杂度筛出素数,还能利用积性函数的性质,顺手把欧拉函数表也递推计算出来。这就是 O(N)O(N) 的魔法。


1 前置知识:积性函数

理解线性筛求 φ\varphi 的前提,是理解欧拉函数的积性

定义:如果不相等的两个数 a,ba, b 互质(即 gcd(a,b)=1\gcd(a, b) = 1),那么:

φ(a×b)=φ(a)×φ(b) \varphi(a \times b) = \varphi(a) \times \varphi(b)

这个性质非常关键,它是我们在筛法中处理 i % p != 0 情况的理论基础。


2 核心逻辑:三种情况的递推

线性筛的核心机制是:确保每个合数只被它的“最小质因子”筛掉。 在筛选过程中,我们从小到大枚举数值 ii,并枚举已有的素数 pp。我们要计算的是 φ(i×p)\varphi(i \times p)

根据 iipp 的关系,分为三种情况:

情况 1:ii 是质数

这是最基础的情况。根据欧拉函数的定义,质数 pp 只有 11 和它本身两个因子,与小于它的 p1p-1 个数都互质。

φ(i)=i1 \varphi(i) = i - 1

(在代码中,这通常在判断出质数时直接赋值)

情况 2:imodp0i \bmod p \neq 0iipp 互质)

此时,pp 是质数,且 pp 不是 ii 的因子。显然 gcd(i,p)=1\gcd(i, p) = 1。 利用欧拉函数的积性

φ(i×p)=φ(i)×φ(p)=φ(i)×(p1) \begin{aligned} \varphi(i \times p) &= \varphi(i) \times \varphi(p) \\ &= \varphi(i) \times (p - 1) \end{aligned}

情况 3:imodp=0i \bmod p = 0iipp 不互质,核心难点!)

imodp=0i \bmod p = 0 时,说明 pp 已经是 ii 的因子了(实际上是最小质因子)。 这意味着:i×pi \times p 的质因子种类,与 ii 的质因子种类完全相同。 pp 并没有带来新的质因子,只是让原有的质因子 pp 的指数加 1 了。

让我们看欧拉函数的通项公式:

φ(n)=n×k=1m(11pk) \varphi(n) = n \times \prod_{k=1}^{m} (1 - \frac{1}{p_k})
  • 对于 ii
    φ(i)=i×(11pk)\varphi(i) = i \times \prod (1 - \frac{1}{p_k})
  • 对于 i×pi \times p
    φ(i×p)=(i×p)×(11pk)=p×[i×(11pk)]=p×φ(i) \begin{aligned} \varphi(i \times p) &= (i \times p) \times \prod (1 - \frac{1}{p_k}) \\ &= p \times \left[ i \times \prod (1 - \frac{1}{p_k}) \right] \\ &= p \times \varphi(i) \end{aligned}

结论:当 imodp=0i \bmod p = 0 时,递推公式为:

φ(i×p)=φ(i)×p \varphi(i \times p) = \varphi(i) \times p

我们通过两个具体的例子来剖析 i % p == 0 的情况。


例子 1:普通合数的情况

假设当前枚举到:

  • i=6i = 6
  • p=2p = 2pp66 的最小质因子,满足 6 % 2 == 0

我们要计算的目标是 φ(12)\varphi(12) (即 φ(6×2)\varphi(6 \times 2))。

1. 先看 φ(i)\varphi(i)φ(6)\varphi(6) 66 的质因子是 {2,3}\{2, 3\}。 根据通项公式:

φ(6)=6×(112)×(113)质因子部分=6×12×23=2 \varphi(6) = 6 \times \underbrace{\left(1 - \frac{1}{2}\right) \times \left(1 - \frac{1}{3}\right)}_{\text{质因子部分}} = 6 \times \frac{1}{2} \times \frac{2}{3} = 2

2. 再看 φ(i×p)\varphi(i \times p)φ(12)\varphi(12) 1212 的质因子依然是 {2,3}\{2, 3\}。(虽然多乘了一个 22,但并没有引入新的质数,比如 5577)。 根据通项公式:

φ(12)=12×(112)×(113)质因子部分完全没变! \varphi(12) = 12 \times \underbrace{\left(1 - \frac{1}{2}\right) \times \left(1 - \frac{1}{3}\right)}_{\text{质因子部分完全没变!}}

3. 对比发现: 你会发现 φ(12)\varphi(12)φ(6)\varphi(6) 的公式中,后面括号里的那一长串完全一样。 唯一的区别就是前面的系数从 66 变成了 1212(扩大了 22 倍,即 pp 倍)。

所以:

φ(12)=φ(6)×2=2×2=4 \varphi(12) = \varphi(6) \times 2 = 2 \times 2 = 4
结果正确。


例子 2:纯素数幂次的情况

假设当前枚举到:

  • i=4i = 4 (222^2)
  • p=2p = 2 (满足 4 % 2 == 0

我们要计算的目标是 φ(8)\varphi(8) (即 φ(4×2)\varphi(4 \times 2))。

1. 先看 φ(4)\varphi(4) 44 的质因子只有 {2}\{2\}

φ(4)=4×(112)=2 \varphi(4) = 4 \times \left(1 - \frac{1}{2}\right) = 2

2. 再看 φ(8)\varphi(8) 88 (232^3) 的质因子依然只有 {2}\{2\}。并没有因为多乘了个 22 就变成了别的。

φ(8)=8×(112) \varphi(8) = 8 \times \left(1 - \frac{1}{2}\right)

3. 结论: 同样,后面括号里的部分完全没变。只有前面的系数乘了 pp

φ(8)=φ(4)×2=2×2=4 \varphi(8) = \varphi(4) \times 2 = 2 \times 2 = 4
结果正确。


反面教材:如果误用了“积性函数”公式会怎样?

有些同学会习惯性地认为 φ(A×B)=φ(A)×φ(B)\varphi(A \times B) = \varphi(A) \times \varphi(B) 永远成立。 这是错误的!这只在 gcd(A,B)=1\gcd(A, B)=1 时成立。

如果在 i % p == 0 时(比如 i=6,p=2i=6, p=2)强行套用积性公式:

  • 错误计算φ(6×2)=?φ(6)×φ(2)=2×1=2\varphi(6 \times 2) \stackrel{?}{=} \varphi(6) \times \varphi(2) = 2 \times 1 = 2
  • 正确值φ(12)=4\varphi(12) = 4

为什么错了? 因为 6622 不互质。它们共享了因子 22。 当我们计算 φ(6)\varphi(6) 时,已经乘过一次 (112)(1 - \frac{1}{2}) 了。 当我们计算 φ(2)\varphi(2) 时,又乘了一次 (112)(1 - \frac{1}{2})。 如果直接相乘,会导致 (112)(1 - \frac{1}{2}) 被乘了两次,导致结果偏小。

而线性筛的正确公式 φ(i×p)=φ(i)×p\varphi(i \times p) = \varphi(i) \times p 刚好避免了这个问题,因为它只调整了前面的倍数 nn,没有动后面的 (11/p)\prod(1 - 1/p)


3 C++ 代码模板

这是标准的模板代码,包含了详细的注释,建议理解并背诵。

cpp
copy
        
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <iostream> #include <vector> using namespace std; const int MAXN = 1000005; int phi[MAXN]; // 存储欧拉函数值 vector<int> primes; // 存储质数 bool st[MAXN]; // st[i] = true 代表 i 是合数(被筛掉了) void get_phi_linear(int n) { // 1. 初始化边界 phi[1] = 1; // 2. 线性筛过程 for (int i = 2; i <= n; ++i) { // --- 情况 1: i 是质数 --- if (!st[i]) { primes.push_back(i); phi[i] = i - 1; // 质数的欧拉函数是 p-1 } // 枚举已有的质数 p,作为最小质因子去筛合数 (i * p) for (int p : primes) { // 越界检查 if (i * p > n) break; // 标记合数 st[i * p] = true; if (i % p == 0) { // --- 情况 3: p 是 i 的因子 (Break条件) --- // i * p 的最小质因子是 p,且 p 已经包含在 i 的质因子分解中了 // 此时质因子种类不变,仅数值扩大 p 倍 phi[i * p] = phi[i] * p; // 【关键 Break】 // 保证每个合数只被它的最小质因子筛掉。 break; } else { // --- 情况 2: p 和 i 互质 --- // 利用欧拉函数的积性 phi[i * p] = phi[i] * (p - 1); } } } } int main() { get_phi_linear(100); cout << "phi(10) = " << phi[10] << endl; // 4 cout << "phi(12) = " << phi[12] << endl; // 4 cout << "phi(13) = " << phi[13] << endl; // 12 return 0; }

4 记忆口诀

在考场上,为了快速准确地写出代码,可以记忆这三个分支:

  1. 遇见质数φ(i)=i1\varphi(i) = i - 1
  2. 互质乘积i % p != 0):积性生效,φ(ip)=φ(i)×(p1)\varphi(ip) = \varphi(i) \times (p - 1)
  3. 包含因子i % p == 0):直接乘倍数,φ(ip)=φ(i)×p\varphi(ip) = \varphi(i) \times p

5 复杂度分析

  • 时间复杂度O(N)O(N)。 得益于 if (i % p == 0) break; 这一行,每个合数 xx 只会被它的最小质因子筛除一次。这意味着循环体内的操作次数严格等于 NN 的量级。
  • 空间复杂度O(N)O(N)。 需要 phi 数组和 st 数组(或 primes 数组)来存储数据。

总结

线性筛不仅仅是用来找质数的,它是一把处理积性函数的“瑞士军刀”。

只要掌握了这个模板,你就可以通过修改几行递推公式,轻松求出:

  • 莫比乌斯函数 μ(n)\mu(n)
  • 约数个数 d(n)d(n)
  • 约数和 σ(n)\sigma(n)

它们的逻辑完全一致:在 i % p == 0i % p != 0 时分别套用对应的数学性质。掌握了它,你就拿到了通往高级数论题目的入场券!

练习题目