质数筛法
质数筛法
引入
原题链接:P3912 素数个数 - 洛谷
求 \(1\sim n\) 有多少个质数
朴素求法,时间复杂度 \(O(n\sqrt{n})\)
import java.util.Scanner;
public class Main {
static boolean isPrime(int x) {
if (x < 2) return false;
for (int i = 2, sqrt = (int) Math.sqrt(x); i <= sqrt; ++i)
if (x % i == 0) return false;
return true;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
final int n = sc.nextInt();
int cnt = 0;
for (int i = 1; i <= n; ++i) {
if (isPrime(i)) ++cnt;
}
System.out.println(cnt);
}
}
时间复杂度太高,考虑其他解法
埃氏筛
对于任意一个大于 \(1\) 的正整数 \(x\),那么他的 \(k\) 倍一定是合数(\(k>1\)),因为该数可以表示成 \(k\times x\)。
利用这个结论,如果我们从小到大考虑每个数,然后同时把当前这个数的所有(比自己大的)倍数记为合数,那么运行结束的时候没有被标记的数就是素数了。
时间复杂度 \(O(n\log\log n)\)
实现
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
final int n = sc.nextInt();
boolean[] isPrime = new boolean[n + 1];
// 初始化2~n均为素数,逐个筛去
for (int i = 2; i <= n; ++i) isPrime[i] = true;
for (int i = 2; i <= n; ++i) {
for (int j = 2 * i; j <= n; j += i) {
isPrime[j] = false;
}
}
int cnt = 0;
for (boolean is : isPrime)
if (is) ++cnt;
System.out.println(cnt);
}
}
优化
一、只筛质数的倍数
对于一个合数 \(a\times b\)
如果 \(a\) 是合数,且 \(a\) 是被 \(x\) 筛去的
那么合数 \(a\times b\) 会在筛 \(x\) 的倍数时被筛去
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
final int n = sc.nextInt();
boolean[] isPrime = new boolean[n + 1];
for (int i = 2; i <= n; ++i) isPrime[i] = true;
for (int i = 2; i <= n; ++i) {
if (isPrime[i]) {
for (int j = 2 * i; j <= n; j += i) {
isPrime[j] = false;
}
}
}
int cnt = 0;
for (boolean is : isPrime)
if (is) ++cnt;
System.out.println(cnt);
}
}
二、内循环从 \(i^2\) 筛其倍数
对于一个合数 \(a\times b\),其中 \(a>b>1\)
在用 \(b\) 筛掉这个合数之前,一定会被 \(a\) 先筛去,即一个合数会先被其较小的因子筛去
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
final int n = sc.nextInt();
boolean[] isPrime = new boolean[n + 1];
for (int i = 2; i <= n; ++i) isPrime[i] = true;
for (int i = 2; i <= n; ++i) {
// 要注意乘法是否会超出int范围
if (isPrime[i] && (long) i * i <= n) {
for (int j = i * i; j <= n; j += i) {
isPrime[j] = false;
}
}
}
int cnt = 0;
for (boolean is : isPrime)
if (is) ++cnt;
System.out.println(cnt);
}
}
三、外循环只筛至 \(\sqrt n\)
上一个优化中提到,一个合数会先被其较小的因子筛去。
而对于 \([\sqrt n,n]\) 内的数均会被小于 \(\sqrt n\) 的因子筛去
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
final int n = sc.nextInt();
boolean[] isPrime = new boolean[n + 1];
for (int i = 2; i <= n; ++i) isPrime[i] = true;
for (int i = 2, sqrt = (int) Math.sqrt(n); i <= sqrt; ++i) {
if (isPrime[i]) {
for (int j = i * i; j <= n; j += i) {
isPrime[j] = false;
}
}
}
int cnt = 0;
for (boolean is : isPrime)
if (is) ++cnt;
System.out.println(cnt);
}
}
四、只筛奇数
我们知道除 \(2\) 以外的偶数均不是合数,因为所有偶数都可以写成 \(2\times x\) 的形式
对此,可以选择只筛奇数
奇数可以表示为 \(2\times x+1\),其中 \(x\in N\) ,\(x\) 对应质数表的下标
因此,一个数 \(n\) ,判断其是否为质数时,寻找下标为 \(\lfloor\dfrac{n}{2}\rfloor\) 的质数表所对应的值即可。
对于数组长度:
-
当 \(n\) 为偶数时,\(1\sim n\) 中最大的奇数为 \(n-1\)
要用 \(2\times idx+1\) 表示 \(n-1\),即 \(idx_{max}=\dfrac{n-2}{2}\)
因此需要空间个数为 \(\dfrac{n-2}{2}+1=\dfrac{n}{2}\) -
当 \(n\) 为奇数时,\(1\sim n\) 中最大的奇数为 \(n\)
要用 \(2\times idx+1\) 表示 \(n\),即 \(idx_{max}=\dfrac{n-1}{2}\)
因此需要空间个数为 \(\dfrac{n-1}{2}+1=\dfrac{n+1}{2}\)
综上,需要数组空间为 \(\lceil\dfrac{n}{2}\rceil=\lfloor\dfrac{n+1}{2}\rfloor\)
注意此时每次增量应为原来的两倍
import java.util.Scanner;
public class Main {
static boolean[] primeTable;
static void initialize(final int n) {
primeTable = new boolean[(n + 1) / 2];
for (int i = 1; i < primeTable.length; ++i) primeTable[i] = true;
for (int i = 3, sqrt = (int) Math.sqrt(n); i <= sqrt; i += 2) {
if (primeTable[i / 2]) {
for (int j = i * i; j <= n; j += 2 * i) {
primeTable[j / 2] = false;
}
}
}
}
static boolean isPrime(int x) {
if (x % 2 == 0) return x == 2;
return primeTable[x / 2];
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
final int n = sc.nextInt();
initialize(n);
int cnt = n < 2 ? 0 : 1;
for (boolean is : primeTable)
if (is) ++cnt;
System.out.println(cnt);
}
}
线性筛
埃氏筛仍有优化思路,因为一些合数可能会被多个因子筛去
算术基本定理
任何一个大于 \(1\) 的自然数 \(N\),如果 \(N\) 不为质数,那么 \(N\) 可以唯一分解成有限个质数的乘积 \(N=P_1^{a_1}P_2^{a_2}P_3^{a_3}\cdots P_n^{an}\),这里 \(P_1<P_2<P_3\cdots<P_n\) 均为质数,其中指数 \(a_i\) 是正整数。这样的分解称为 \(N\) 的标准分解式。
我们选择用每个合数的最小质因子(或者说用这个合数的最大的因子)来筛去这个合数
这样就保证了每个合数只被筛去一次,那么时间复杂度就降低至 \(O(n)\) 了
实现
将每次遇到的质数存至数组中,若当前遍历到的数记为 \(i\),目前所存的质数为 \(primes_j\),其中\(j>=0\)。
则每次只筛取以 \(primes_j\) 为最小质因子, \(i\) 为最大因子的合数
当 \(i\) 是 \(primes_j\) 的因子时,说明 \(i\) 可以分解出 \(primes_j\) 这个质数
又因为 \(primes_k\),其中 \(j<k<tot\) ,一定大于 \(primes_j\)
因此,合数 \(i\times primes_k\) 一定会被 \(primes_j\)筛去
举个例子:
当 \(i=4\) 时,此时 \(primes=2,3\),先标记 \(4\times 2\) 为合数,此时发现 \(2\) 是 \(4\) 的一个因子,则不再继续往后筛,因为 \(4\times 3= 2\times 2\times 3=2\times 6\),
当 \(i=6\) 时,此时 \(primes=2,3,5\),先标记 \(6\times 2\) 为合数,此时发现 \(2\) 是 \(6\) 的一个因子,则不再继续往后筛,因为 \(6\times 3=2\times 3\times 3=2\times 9\) , \(6\times 5=2\times 3\times 5=2\times 15\)
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
int n = new Scanner(System.in).nextInt();
//除2以外的偶数一定不是质数,只需使用一半空间
int[] primes = new int[(n + 1) / 2];
boolean[] isPrime = new boolean[n + 1];
//tot记录当前质数个数
int tot = 0;
for (int i = 2; i <= n; ++i) isPrime[i] = true;
for (int i = 2; i <= n; ++i) {
if (isPrime[i]) primes[tot++] = i;
for (int j = 0; j < tot && i * primes[j] <= n; ++j) {
isPrime[i * primes[j]] = false;
if (i % primes[j] == 0) break;
}
}
System.out.println(tot);
}
}
PS:注意到筛法求素数的同时也得到了每个数的最小质因子,这是筛法求积性函数的铺垫
存储质数数组空间大小
/*
*@ 作用:欧拉筛1~n的素数
*@ 返回值:1~n内素数个数
*@ primes[i] : 第i+1个素数
*@ isPrime[i] : true则i是质数
*/
static int[] primes;
static boolean[] isPrime;
public static int getPrime(final int n) {
int len = (int) Math.max((n + 1) / (Math.log(n + 1) - 1.112), 1);
primes = new int[len];
isPrime = new boolean[n + 1];
Arrays.fill(isPrime, true);
isPrime[0] = isPrime[1] = false;
int tot = 0;
for (int i = 2, j; i <= n; ++i) {
if (isPrime[i]) primes[tot++] = i;
for (j = 0; j < tot && i * primes[j] <= n; ++j) {
isPrime[i * primes[j]] = false;
if (i % primes[j] == 0) break;
}
}
return tot;
}