1. 有时候我们给出长度为N的区间需要求解出这个区间中质数的个数,假如使用素数定理来求解出的话时间复杂度可能有点高,因为使用素数定理的时候核心代码如下:
for(int i = 2;i < x; i++){
if(arr[i]!=0){
continue;
}
int k = 2;
while(i * k < x){
arr[i * k] = -1;
k++;
}
}
上面的代码可能存在重复标记的情况,因为循环到某个数字为质数的情况下,那么会进入下面的while循环,k从2开始计算那么假如当循环到质数7的时候进入while循环然后再将arr[ 14 ], arr[ 28 ]... 标记为-1 而arr[ 14 ]原来当质数为2的时候就标记过那么就可能存在着多个标记重复的情况,所以当区间比较大的情况下那么使用素数定理来解决的时候时间复杂度是比较高的,那么我们可以使用另外一种方法来筛选素数从而降低在筛选素数的时间复杂度,这种方法是:Eular素数筛选定理
与Eratosthenes筛法不同的是,对于外层枚举i,无论i是质数,还是是合数,我们都会用i的倍数去筛,但在枚举的时候,我们只枚举i的质数倍。比如2i,3i,5i,...,而不去枚举4i,6i...,原因我们后面会讲到。
此外,在从小到大依次枚举质数p来计算i的倍数时,我们还需要检查i是否能够整除p。若i能够整除p,则停止枚举。
利用该算法,可以保证每个合数只会被枚举到一次。我们可以证明如下命题:
假设一个合数k=M*p1,p1为其最小的质因子。则k只会在i=M,primeList[j]=p1时被筛掉一次。
首先会在i=M,primeList[j]=p1时被筛掉是显然的。因为p1是k的最小质因子,所以i=M的所有质因子也≥p1。于是j循环在枚举到primeList[j]=p1前不会break,从而一定会在i=M,primeList[j]=p1时被筛掉
其次不会在其他时候被筛掉。否则假设k在i=N, primeList[j]=p1时被筛掉了,此时有k=N*p2。由于p1是k最小的质因子,所以p2 > p1,M > N且p|N。则i=N,j枚举到primeList[j]=p1时(没到primeList[j]=p2)就break了。所以不会有其他时候筛掉k。
同时,不枚举合数倍数的原因也在此:对于一个合数k=M*2*3。只有在枚举到i=M*3时,才会计算到k。若我们枚举合数倍数,则可能会在i=M时,通过M*6计算到k,这样也就造成了多次重复计算了。
综上,Eular筛法可以保证每个合数只会被枚举到一次,时间复杂度为O(n)。当N越大时,其相对于Eratosthenes筛法的优势也就越明显
当循环到某个数为质数的时候那么该质数通过循环遍历乘以存放primeList数组中原来存放的质数来标记处区间N中的合数
2. 具体的代码如下:
import java.util.Scanner;
public class Main{
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
long res = solve(n);
System.out.println(res);
sc.close();
}
private static long solve(int n) {
int prime[] = new int[n + 1];
int primeCount = 0;
for(int i = 2; i <= n; i++){
if(prime[i] == 0){
//存放质数方便后面标记合数
prime[primeCount++] = i;
//System.out.println(i);
}
for(int j = 0; j <= primeCount && i * prime[j] <= n; j++){
prime[i * prime[j]] = 1;
//精髓
if(i % prime[j] == 0)break;
}
}
return primeCount;
}
}