Day6

你提供的这段代码是欧拉筛(Euler Sieve / Linear Sieve)最标准、最精炼的写法,常用于算法竞赛。

这段代码之所以被称为“线性筛”,是因为它保证了范围内的每一个合数都被其“最小质因子”筛掉,且仅被筛掉一次

下面我为你逐行深度解析这段代码的逻辑:

1. 初始化

memset(st, false, sizeof(st));
  • st[i]false 表示 i 是素数,为 true 表示 i 是合数。
  • 在多组测试数据的情况下,memset 是必须的。

2. 外层循环

for(int i = 2; i <= max_val; i ++) {
    if(!st[i]) Pri[cnt ++] = i; // 如果没被筛掉,说明 i 是素数
  • 从 2 遍历到 max_val
  • 如果 st[i]false,说明在之前的循环里没有更小的质因子能把它筛掉,那它一定就是质数,加入 Pri 数组。

3. 内层循环(核心)

for(int j = 0; j < cnt && Pri[j] <= max_val / i; j ++) {
    st[Pri[j] * i] = true; // 标记合数
  • j < cnt:遍历当前已经发现的所有质数。
  • Pri[j] <= max_val / i:这是一种防溢出的写法,等价于 Pri[j] * i <= max_val。这保证了我们要筛掉的合数在预定范围内。
  • 关键点:这里是用 “当前的数 \(i\)” × “某个质数 \(Pri[j]\) 来构造一个合数。

4. 线性复杂度的精髓

if(i % Pri[j] == 0) break;

这是全篇最重要的逻辑。为什么这一行能保证 \(O(n)\)

  • 逻辑拆解

    1. 如果 i % Pri[j] == 0,说明 Pri[j]i 的最小质因子(因为质数是从小到大遍历的)。
    2. 那么对于下一个要处理的合数 Pri[j+1] * i
      • 我们可以把 i 写成 \(k \times Pri[j]\)
      • 所以 Pri[j+1] * i = Pri[j+1] * k * Pri[j]
      • 显然,这个新合数的最小质因子应该是 Pri[j],而不是 Pri[j+1]
    3. 结论:如果我们在此时不 break,那么 Pri[j+1] * i 就会被 Pri[j+1] 筛掉。但这违背了“每个数只被最小质因子筛掉”的原则(它应该在后面 \(i\) 变大到某个值时,被 Pri[j] 筛掉)。
  • 结果:由于每个数只被其最小质因子筛一次,整个双层循环的总运行次数严格等于 max_val,复杂度为 \(O(max\_val)\)


举例运行过程:筛 30 以内的数

i = 4 时:

  1. 4 是合数(之前被 2 筛过了),不进入 if(!st[i])
  2. 进入内层循环,此时质数表 Pri{2, 3}
  3. j=0Pri[0]=2。筛掉 2 * 4 = 8
  4. 检查 4 % 2 == 0,成立!Break
    • 思考:如果不 Break,下一步会筛掉 3 * 4 = 12。但 12 的最小质因子是 2,它应该在 i = 6 时被 2 * 6 = 12 筛掉。欧拉筛通过这个 break 避免了重复操作。

i = 9 时:

  1. 9 是合数,进入内层循环,质数表 {2, 3, 5, 7}
  2. j=0Pri[0]=2。筛掉 2 * 9 = 18
  3. j=1Pri[1]=3。筛掉 3 * 9 = 27
  4. 检查 9 % 3 == 0,成立!Break
    • 注意:27 的最小质因子是 3,这里筛掉它是正确的。接下来的 5 * 9 = 45 最小质因子也是 3,不该由质数 5 来筛。

优缺点总结

  • 优点
    • 极速:理论上是最快的筛法。
    • 多功能:在 break 之前或之后,可以顺便处理积性函数(如欧拉函数 \(\phi(n)\)、莫比乌斯函数 \(\mu(n)\)、约数个数等),只需多开一个数组。
  • 缺点
    • 空间消耗:需要一个大的 st 数组和一个 Pri 数组。
    • 局限性:如果 \(max\_val\) 超过 \(10^8\),内存压力会很大(但在大多数算法题中 \(10^7\) 是上限)。

结论:你手里的这段代码是处理中大规模素数问题的“终极武器”。


这是一个非常棒的观察!理解了 \(i\) 的角色和这个等式,你就真正触及了欧拉筛(线性筛)的灵魂。

我们来拆解一下:

1. \(i\) 是什么?

在代码中,i 是外层循环的变量:

for(int i = 2; i <= max_val; i ++)
  • 字面意思:它是一个从 2 到 max_val 扫描的每一个整数。
  • 算法角色:它扮演的是“倍数”(或者是“放大器”)的角色。
  • 核心公式:欧拉筛的核心公式是:合数 = 质数 \(Pri[j]\) × 倍数 \(i\)

在筛选过程中,每一个合数都被拆解成一个“最小质因子”和一个“倍数”。
比如,当 \(i=6\) 时,它会和质数表里的质数相乘,去“干掉” 12 (\(2 \times 6\))。


2. 为什么 \(i\) 能用 \(k \times Pri[j]\) 表示?

这源于代码中的那行判断条件:

if (i % Pri[j] == 0) break;

当你运行到这一行且条件成立时,根据取模运算的定义:
如果 i 除以 Pri[j] 的余数是 0,意味着 i 能被 Pri[j] 整除。

既然能整除,那么一定存在一个整数 \(k\),使得:

\[i = k \times Pri[j] \]

这就是你疑惑的来源。 这个 \(k\) 就是 \(i\) 除以 \(Pri[j]\) 的商。


3. 为什么要因为这个等式而 break?(深度逻辑)

这是最关键的部分。假设我们当前正在处理 i,并且已经发现了 \(i = k \times Pri[j]\)

如果我们不 break,继续看质数表里的下一个质数 \(Pri[j+1]\)
我们接下来会去标记这个合数:

\[\text{待标记合数} = Pri[j+1] \times i \]

现在,我们把 \(i = k \times Pri[j]\) 代入这个合数里:

\[\text{待标记合数} = Pri[j+1] \times (k \times Pri[j]) \]

我们整理一下:

\[\text{待标记合数} = (Pri[j+1] \times k) \times Pri[j] \]

发现了什么?
这个即将被标记的合数,它不仅有 \(Pri[j+1]\) 这个因子,它还有一个因子是 \(Pri[j]\)
由于质数表是从小到大排列的,显然 \(Pri[j] < Pri[j+1]\)

这意味着:
\(Pri[j+1] \times i\) 这个合数的“最小质因子”是 \(Pri[j]\),而不是 \(Pri[j+1]\)

如果我们现在(在倍数为 \(i\) 的时候)用 \(Pri[j+1]\) 去筛掉它,它就是被“非最小质因子”筛掉了。等到后面,当倍数 \(i\) 变大到 \((Pri[j+1] \times k)\) 的时候,它又会被 \(Pri[j]\) 筛一次。

这就导致一个合数被筛了多次,算法就不再是线性的了。


4. 形象的例子:当 \(i = 6\)

  1. 质数表里有 Pri = {2, 3, 5, ...}
  2. 当外层循环 \(i = 6\) 时:
    • \(j=0, Pri[j]=2\):标记 \(2 \times 6 = 12\)
    • 检查 \(6 \% 2 == 0\)成立!。此时 \(i = 3 \times 2\)(这里的 \(k=3, Pri[j]=2\))。
    • 触发 Break!

如果不 Break 会发生什么?

  • 程序会继续 \(j=1, Pri[j]=3\)
  • 标记 \(3 \times 6 = 18\)
  • 问题来了\(18\) 的最小质因子是 \(2\),不是 \(3\)
  • 如果你现在筛掉了 \(18\),那么以后当 \(i = 9\) 时,程序还会执行 \(2 \times 9 = 18\) 又筛一次。
  • \(18\) 就被筛了两遍!

总结

  • \(i\):是合数拆解后的那个“大头”(倍数)。
  • \(i = k \times Pri[j]\):说明 \(Pri[j]\)\(i\) 的因子,更是 \(i \times \text{后续质数}\) 的最小质因子。
  • Break:是为了把“用最小质因子筛选”这个重任,留给后面更大的 \(i\)。这保证了 \(O(n)\) 的神速。
posted @ 2026-01-27 15:29  EcSilvia  阅读(44)  评论(0)    收藏  举报