给定一个正整数 n,请你求出 1∼n 中质数的个数。
输入格式
共一行,包含整数 n。
输出格式
共一行,包含一个整数,表示 1∼n 中质数的个数。
数据范围
1≤n≤106
输入样例:
8
输出样例:
4
朴素做法
for循环 2~n
,
判断当前数是不是合数,不是的话把他加入素数集合中
for循环将当前数的倍数都断定为合数
时间复杂度: O(nlogn)
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int res = getPrimers(n);
System.out.println(res);
}
static int getPrimers(int n) {
boolean[] st = new boolean[n + 1];
//这个数组其实有没有都行,因为目的是求质数个数,不是每个质数
//后面的线性筛法需要,所以保留一下
int[] primers = new int[n];
int idx = 0;
for (int i = 2; i <= n; i++) {
//判断当前数是否为素数
if (!st[i]) primers[idx++] = i;
//用当前数更新后面的数
for (int j = i; j <= n; j += i) {
st[j] = true;
}
}
return idx;
}
}
埃氏筛法
相较于线性筛法,将内层for循环放入if内,意思是只用质因子更新后面的合数。
这也很好理解,因为一个合数必然能被分解为质因子的乘积。
时间复杂度: O(nloglogn)
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int res = getPrimers(n);
System.out.println(res);
}
static int getPrimers(int n) {
boolean[] st = new boolean[n + 1];
int[] primers = new int[n];
int idx = 0;
for (int i = 2; i <= n; i++) {
//判断当前数是否为素数
if (!st[i]) {
primers[idx++] = i;
//用素数更新后面的数
for (int j = i; j <= n; j += i) {
st[j] = true;
}
}
}
return idx;
}
}
线性筛法
相较于埃氏筛法又有了改进。
对于埃氏筛法来说,有的合数会被筛多次,例如:求1-100中的质数,对于12
来说,当i遍历到2
时st[12]
被置为true
,当i
遍历到3
时,st[12]
又被置为true
。也就是说一个合数会被所有比它小的质因子都筛一遍。
线性筛法的原理就是每个合数都是被它的最小质因子筛掉的
时间复杂度O(n)
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int res = getPrimers(n);
System.out.println(res);
}
static int getPrimers(int n) {
boolean[] st = new boolean[n + 1];
int[] primers = new int[n];
int idx = 0;
for (int i = 2; i <= n; i++) {
//判断当前数是否为素数
if (!st[i]) primers[idx++] = i;
for (int j = 0; primers[j] <= n / i; j++) {
//当i % primers[j] != 0时primers[j]与i互质,i*primers[j]的最小质因子就是primers[j]
st[primers[j] * i] = true;
//当i % primers[j] == 0 时,
//i的最小质因子是primers[j],所以i*primers[j]的最小质因子就是primers[j]
if (i % primers[j] == 0) break;
}
}
return idx;
}
}
// 20220405 新版代码
几个问题
- 内层
if
一定能使内层for
循环结束吗?
答:一定能。因为primers数组存储了 2~i
之间的所有质因子
如果i
是质数,一定能结束,因为primers数组当前的最后一个质数就是i
, i % i == 0
如果i
是合数,也一定能结束,因为i
能被分解为质数的乘积且这些质数一定小于i
,由于primers中已经存储了 2~i
的所有质数,当 primers[j]
为i
的最小质因子时 i % primers[j] == 0
- 合数一定都会被筛掉吗?
答:一定会。因为合数能分解为质数的乘积,考虑这样一种分解,对于合数k
,假定它的最小质因子为a
,那么它的另一个质因子 b = k / a
。
外层循环i
的范围是 2~n
,包含了所有b
的范围,也就是说,只要我们能找到k
的最小质因子a
,就可以筛掉 k = a * b
如果当前i
是质数,i
一定会被存入 primers
数组的当前最后一个位置上。只要 primers[j] * i <= n
,就能筛掉一个合数
如果当前i
是合数,i
的最小质因子一定在primers
中,当 i % primer[j] == 0
时说明当前i
的最小质因子就是primers[j]
,那么对于 i * primers[j]
来说,它的最小质因子还是primers[j]
,应该被筛掉。且此时应退出内层循环,不能再继续筛,因为此时 i * primers[j]
不是被primers[j]
筛掉的,而是被i
的最小质因子筛掉的
应用
:::tips 如果题目要求对多个数字进行算术基本定理分解,可以考虑结合线性筛进行快速计算 :::
AcWing 1295. X的因子链
import java.util.*;
public class Main {
static int N = (1 << 20) + 10;
static int[] primers = new int[N], minp = new int[N];
static boolean[] st = new boolean[N];
static int cnt;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
get_primers(N - 1);
while (sc.hasNext()) {
int x = sc.nextInt();
deal(x);
}
}
static void deal(int x) {
int[] c = new int[30];
int tot = 0, idx = 0;
while (x > 1) {
int k = minp[x], cc = 0;
while (x % k == 0) {
cc++;
tot++;
x /= k;
}
c[idx++] = cc;
}
long res = 1;
for (int i = 1; i <= tot; i++)
res *= i;
for (int i = 0; i < idx; i++) {
for (int j = 1; j <= c[i]; j++)
res /= j;
}
System.out.println(tot + " " + res);
}
static void get_primers(int n) {
for (int i = 2; i <= n; i++) {
if (!st[i]) {
minp[i] = i;
primers[cnt++] = i;
}
for (int j = 0; primers[j] * i <= n; j++) {
st[primers[j] * i] = true;
minp[primers[j] * i] = primers[j];
if (i % primers[j] == 0) break;
}
}
}
}
AcWing 196. 质数距离
思路:
直接用线性筛会爆空间,爆时间
考虑到左右边界之间相差只有106
,想要快速找出这一段区间内的质数,有一个Trick
如果一个数x
是合数,一定存在一个小于等于sqrt(x)
的质数是它的因子。
故可以考虑用线性筛法预处理50000
以内的所有质数
对于一段给定区间,用50000
以内的素数快速判断区间中的每个数是不是合数
总的时间复杂度包括预处理 + 求一段区间内的质数
前者为线性,后者可以类比埃氏筛法的时间复杂度,基本是线性
故总时间复杂度为线性
实现时需要考虑溢出问题!以及1不是素数!