线性筛素数

204. 计数质数

线性筛(通常被称为欧拉筛)的核心目的只有一个:把寻找质数的时间复杂度压缩到终极的 O ( n ) O(n) O(n)。它的绝招在于"不走冤枉路"。

传统的埃氏筛法会重复标记同一个合数(比如 12 既被 2 筛除,又被 3 筛除),而线性筛通过极其巧妙的规则避免了这种浪费。它的原理可以浓缩为以下 3 个核心点:

  • 唯一击杀原则 :确保每一个合数,只被它自己的"最小质因数"筛掉一次
  • 边找边筛 :从 2 开始往后遍历数字 i i i。如果 i i i 没被别人标记过,那它就是质数,直接收入质数表。接着,用 i i i 依次去乘以目前已经找到的质数 p p p,把它们的乘积 i × p i \times p i×p 标记为合数。
  • 灵魂刹车(最核心逻辑) :在内层乘法循环里,如果发现 i   m o d   p = = 0 i \bmod p == 0 imodp==0,必须立刻打断循环 !因为这说明 i i i 里面已经包含了 p p p 这个因子。如果继续拿 i i i 去乘后面更大的质数,算出来的结果以后肯定会被当前的 p p p(也就是更小的质因数)提前筛掉,现在就不必多此一举了。

完整代码如下:

cpp 复制代码
class Solution {
public:
    int countPrimes(int n) {
        int idx = 0;
        vector<bool> st(n, false);
        vector<int> primes(n);
        for(int i = 2; i < n; i ++ ) {
            if(!st[i]) primes[idx ++ ] = i;
            for(int j = 0; j < idx && primes[j] * i < n; j ++ ) {
                st[i * primes[j]] = true;
                if(i % primes[j] == 0)  break;
            }
        }
        return idx;
    }
};

注意,当 primes[j] * i 可能溢出时,我们不能将筛素数的判断写成:

cpp 复制代码
for(int j = 0; j < idx && primes[j] < n / i; j ++ )

因为 n / i 是下取整,这会导致我们漏掉部分解。

直接用 (long long) 转换就行了。