埃氏筛
我们考虑这样一个事实:如果 x x x 是质数,那么大于 x x x 的 x x x 的倍数 2 x 2x 2x, 3 x 3x 3x,…一定不是质数,因此我们可以从这里入手。
我们设 i s P r i m e [ i ] isPrime[i] isPrime[i] 表示数 i i i 是不是质数,如果是质数则为 1 1 1,否则为 0 0 0。从小到大遍历每个数,如果这个数为质数,则将其所有的倍数都标记为合数(除了该质数本身),即 0 0 0,这样在运行结束的时候我们即能知道质数的个数。
这里还可以继续优化,因为对于一个质数 x x x,如果按上文说的我们从 2 x 2x 2x开始标记其实是冗余的,应该直接从 x ⋅ x x\cdot x x⋅x 开始标记,因为 2 x , 3 x , … 2x,3x,… 2x,3x,…这些数一定在之前就被其他数的倍数标记过了,例如 2 2 2的所有倍数, 3 3 3的所有倍数等。
class Solution {
public:
int countPrimes(int n) {
vector<int> isPrime(n,1);
int res = 0;
for(long long i=2;i<n;i++) {
if(isPrime[i]) {
++res;
for(long long j=i*i; j<n; j+=i) {
isPrime[j] = 0;
}
}
}
return res;
}
};
线性筛
埃氏筛会有重复标记的行为,比如45会被3和5重复标记,线性筛则可以避免这种情况,线性筛又称欧拉筛,欧拉筛需要维护一个质数表,对于 2 ≤ i < n 2 \le i < n 2≤i<n ,当遍历到整数 i i i 时,执行如下操作,
1、如果 i i i 是质数,即 i i i 没有被标记为合数,则将 i i i 加入质数表
2、从小到大遍历质数列表,对每个质数 p r i m e prime prime ,当 i ∗ p r i m e < n i * prime < n i∗prime<n 时执行如下操作:
a、将 i ∗ p r i m e i*prime i∗prime 标记为合数
b、如果 i i i 能被 p r i m e prime prime 整除,则结束遍历质数列表
这里的原理是,如果如果 i i i 能被 p r i m e prime prime 整除,则记 p r i m e ′ prime' prime′ 为大于 p r i m e prime prime 的质数, y = x ∗ p r i m e ′ y=x*prime' y=x∗prime′ ,则有 y = x p r i m e ∗ p r i m e ′ ∗ p r i m e y = \frac{x}{prime} * prime' *prime y=primex∗prime′∗prime ,当遍历到 $\frac{x}{prime} * prime’ $ 时, y y y 会被质数 p r i m e prime prime 标记,由于 p r i m e < p r i m e ’ prime < prime’ prime<prime’ ,所以当 x p r i m e \frac{x}{prime} primex 为整数,即 x m o d p r i m e = = 0 x \ mod \ prime == 0 x mod prime==0 时,这个整数就可以被 p r i m e ′ prime' prime′ 标记,因此 y y y 会被其最小的质因数标记。
由于欧拉筛可以确保每个合数被其最小的质因数标记,因此可以确保每个合数只被标记一次。
class Solution {
public:
int countPrimes(int n) {
vector<int> isPrime(n, 1);
vector<int> Prime;
for (int i = 2; i < n; ++i) {
if (isPrime[i]){
Prime.push_back(i);
}
for(auto j:Prime) {
if ((long long)i * j < n) {
isPrime[i*j] = 0;
} else {
break;
}
if (i % j == 0) break;
}
}
return Prime.size();
}
};
但是,看似线性筛操作的更少,实际上运行时间会更长,因为进行了更多的运算操作