质数
质数,又称为素数。是指在大于1的自然数中,除了1和它本身以外不再有其他因数的自然数。
1. 质数原理
试除法判断质数
- 根据质数的定义,从2到n-1循环一遍,看是否存在数据能整除n即可判断n是否为质数(试除法),伪码描述如下:
// 时间复杂度:O(n)
bool is_prime(int n) {
if (x < 2) return false;
for (int i = 2; i < n; i++)
if (n % i == 0)
return false;
return true;
}
- 优化:注意到:如果d可以整除n,即d|n,那么 n d ∣ n \frac{n}{d}|n dn∣n,约数是成对出现的。这样我们在枚举约数的时候可以只枚举较小的那一个。
// 时间复杂度:O(sqrt(n))
bool is_prime(int n) {
if (n < 2) return false;
for (int i = 2; i <= n / i; i++)
if (n % i == 0)
return false;
return true;
}
分解质因数
- 我们需要将一个数N可因数分解为 p 1 α 1 ∗ p 2 α 2 ∗ . . . ∗ p k α k p_1^{\alpha_1}*p_2^{\alpha_2}*...*p_k^{\alpha_k} p1α1∗p2α2∗...∗pkαk ,方法仍然是试除法。
// 时间复杂度:O(n)
void divide(int n) {
for (int i = 2; i <= n; i++)
if (n % i == 0) { // 如果该条件成立,则i一定是质数
int s = 0;
while (n % i == 0) {
n /= i;
s++;
}
printf("%d %d\n", i, s); // 质因数,个数
}
}
-
分析上述代码,当
n % i == 0
成立时,则i
一定是质数。因为此时n
一定不含有2~i-1
中的任何质数,假设此时i
不是质数,则i
一定能被2~i-1
中的某个质数整除,又i
能整除n
,则n
一定能被2~i-1
中的某个质数整除,矛盾。因此如果条件成立,则i
一定是质数。 -
优化:注意到:n中最多包含一个大于 n \sqrt n n的质因子(可以用反正法证明),因此循环到 n \sqrt n n即可,最后特判一下是否存在一个大于 n \sqrt n n的质因数即可。
// 时间复杂度:O(log(n)) ~ O(sqrt(n))
void divide(int n) {
for (int i = 2; i <= n / i; i++)
if (n % i == 0) { // 如果该条件成立,则i一定是质数
int s = 0;
while (n % i == 0) {
n /= i;
s++;
}
printf("%d %d\n", i, s); // 质因数,个数
}
if (n > 1) printf("%d %d\n", n, 1);
}
- 假设 n = 2 t n=2^t n=2t,则只需要计算 l o g ( n ) log(n) log(n)次即可,因此优化后的代码时间复杂度为 O ( l o g ( n ) ) O(log(n)) O(log(n))~ O ( n ) O(\sqrt n) O(n)之间。
筛质数
-
所谓的筛质数是指:给定一个正整数n,输出1~n中的质数。
-
存在三种筛质数的方法:(1)朴素法筛质数;(2)埃拉托色尼筛选法;(3)线性选法。
-
这三种方法对数据的存储是相同的,如下:
const int N = 1000010;
int primes[N], cnt; // primes[0]~primes[cnt-1]存储的是0~n中所有的质数(从小到大)
bool st[N]; // st[i] == true说明i不是质数
(1)朴素法筛质数
-
基本思路:将2~n中所有数的倍数都删掉,剩余的数据就是质数。
-
从2一直遍历到n,如果当前
st[i]==false
,说明i
是质数,因为它没被2~i-1
筛掉,说明2~i-1
都不能整除i
,根据定义i
是质数。 -
时间复杂度分析:我们要删除2的所有倍数,3的所有倍数,…,因此计算次数为 n 2 + n 3 . . . + n n = n × ( 1 2 + 1 3 . . . + 1 n ) \frac{n}{2} + \frac{n}{3} ... + \frac{n}{n} =n \times (\frac{1}{2} + \frac{1}{3} ... + \frac{1}{n}) 2n+3n...+nn=n×(21+31...+n1),其中 1 2 + 1 3 . . . + 1 n \frac{1}{2} + \frac{1}{3} ... + \frac{1}{n} 21+31...+n1是调和级数,根据,该级数是发散的,当n趋向于无穷大时有: 1 2 + 1 3 . . . + 1 n = l n ( n ) \frac{1}{2} + \frac{1}{3} ... + \frac{1}{n}=ln(n) 21+31...+n1=ln(n),因此时间复杂度为 O ( n × l o g ( n ) ) O(n\times log(n)) O(n×log(n))。
// 朴素法筛质数,时间复杂度:O(n*log(n))
void get_primes(int n) {
for (int i = 2; i <= n; i++) {
if (!st[i]) primes[cnt++] = i;
for (int j = i + i; j <= n; j += i) st[j] = true;
}
}
(2)埃拉托色尼筛选法(埃式筛法)
-
该方法是对(1)的一个优化。
-
基本思路:将2~n中所有质数的倍数都删掉,剩余的数据就是质数。
-
从2一直遍历到n,如果当前
st[i]==false
,说明i
是质数,因为它没被2~i-1
中的质数筛掉,说明2~i-1
中的质数都不能整除i
,则i
是质数。 -
时间复杂度分析:这里大致估计一下,首先由质数定理:1~n中质数的个数大约为 n l n ( n ) \frac{n}{ln(n)} ln(n)n个,本来要筛n个数,但是现在只需要筛大约为 n l n ( n ) \frac{n}{ln(n)} ln(n)n个数,因此时间复杂度为 n / l o g ( n ) × l o g ( n ) = n n / log(n) \times log(n)=n n/log(n)×log(n)=n,为 O ( n ) O(n) O(n)量级的。
-
实际上,埃式筛法准确的时间复杂度为 O ( n × l o g ( l o g ( n ) ) ) O(n \times log(log(n))) O(n×log(log(n))),这是因为我们的计算次数是 n 2 + n 3 + n 5 + . . . = n × ( 1 2 + 1 3 + 1 5 . . . ) = n × l o g ( l o g ( n ) ) \frac{n}{2} + \frac{n}{3} + \frac{n}{5} + ... =n \times (\frac{1}{2} + \frac{1}{3} + \frac{1}{5} ... ) = n \times log(log(n)) 2n+3n+5n+...=n×(21+31+51...)=n×log(log(n))。
// 埃拉托色尼筛选法,时间复杂度:O(n*log(log(n)))
void get_primes(int n) {
for (int i = 2; i <= n; i++) {
if (!st[i]) {
primes[cnt++] = i;
for (int j = i + i; j <= n; j += i) st[j] = true;
}
}
}
(3)线性选法
-
这和前两种方法没有什么关系,其是另一种思路。
-
基本思路:每个数只用它最小的质因子筛掉。
-
时间复杂度分析:因为每个数只会被它最小的质因子筛掉,因此每个数只会被遍历一次,时间复杂度是 O ( n ) O(n) O(n)的,当 n = 1 0 6 n=10^6 n=106时,线性筛法和埃式筛法效率差不多,当 n = 1 0 7 n=10^7 n=107时,线性筛法速度是埃式筛法的两倍。
// 线性选法,时间复杂度:O(n)
void get_primes(int n) {
for (int i = 2; i <= n; i++) {
if (!st[i]) primes[cnt++] = i;
for (int j = 0; primes[j] <= n / i; j++) {
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
- 分析如下:
// 线性选法,时间复杂度:O(n)
void get_primes(int n) {
for (int i = 2; i <= n; i++) {
if (!st[i]) prime[cnt++] = i;
// 这里没必要写 j < cnt, 因为i如果为合数的话,枚举到i的最小质因子后一定会停下来
// i如果为质数的话,枚举到primes[cnt - 1]=i时一定会停下来
for (int j = 0; primes[j] <= n / i; j++) {
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
/**
* primes[j]记为pj,因为从小到大枚举质因子,所以:
* (1) i % pj == 0 说明:pj是i的最小质因子,pj也是 pj*i的最小质因子
* (2) i % pj != 0 说明:pj一定小于i的所有质因子,pj也是 pj*i的最小质因子
* 另外:对于任意一个合数x,都会被筛掉:假设pj是x的最小质因子,当i枚举到
* x/pj 的时候,pj已经被枚举过(pj <= x/pj),x会pj被筛掉
* --> 因为每个合数都会被筛掉,而且每个数只会被它的最小质因子筛掉,因此每个数
* 只会被筛一次,时间复杂度是O(n)的
*/
2. AcWing上的质数题目
AcWing 866. 试除法判定质数
问题描述
-
问题链接:AcWing 866. 试除法判定质数
分析
- 使用优化的方法直接判断即可。
代码
- C++
#include <iostream>
using namespace std;
bool is_prime(int n) {
if (n < 2) return false;
for (int i = 2; i <= n / i; i++)
if (n % i == 0)
return false;
return true;
}
int main() {
int n;
scanf("%d", &n);
while (n--) {
int x;
scanf("%d", &x);
if (is_prime(x)) puts("Yes");
else puts("No");
}
return 0;
}
AcWing 867. 分解质因数
问题描述
-
问题链接:AcWing 867. 分解质因数
分析
- 使用优化的方法直接分解即可。
代码
- C++
#include <iostream>
using namespace std;
void divide(int n) {
for (int i = 2; i <= n / i; i++)
if (n % i == 0) { // 如果该条件成立,则i一定是质数
int s = 0;
while (n % i == 0) {
n /= i;
s++;
}
printf("%d %d\n", i, s); // 质因数,个数
}
if (n > 1) printf("%d %d\n", n, 1);
}
int main() {
int n;
scanf("%d", &n);
while (n--) {
int x;
scanf("%d", &x);
divide(x);
printf("\n");
}
return 0;
}
AcWing 868. 筛质数
问题描述
-
问题链接:AcWing 868. 筛质数
分析
- 上述原理中筛质数的三种方法随便选一种即可。
代码
- C++
#include <iostream>
using namespace std;
const int N = 1000010;
int primes[N], cnt;
bool st[N];
void get_primes(int n) {
for (int i = 2; i <= n; i++) {
if (!st[i]) primes[cnt++] = i;
for (int j = 0; primes[j] <= n / i; j++) {
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
int main() {
int n;
scanf("%d", &n);
get_primes(n);
printf("%d\n", cnt);
return 0;
}
AcWing 1292. 哥德巴赫猜想
问题描述
-
问题链接:AcWing 1292. 哥德巴赫猜想
分析
-
一种做法是对于每个需要分解的数t,我们可以从3开始遍历,然后判断两个数是否是质数,如果是的话,输出答案,考虑下一个数。这样的时间复杂度是: O ( n × n ) O(n \times \sqrt n) O(n×n)的,会超时,因此不可采取。
-
我们可以首先使用线性筛法将所有的质数筛选出来,对于某个需要分解的数
t
,从a=3
开始遍历这些质数,然后判断t-a
是否是质数,如果是的话输出答案,继续处理下一个数据。 -
如何判断
t-a
是否是质数呢?这个可以采取查表的方式进行,在线性筛法中会使用到st
数据,表示是否被筛掉,如果st[t-a]==false
的话说明t-a
是质数。 -
计算量分析:因为
n
最大取 1 0 6 10^6 106,根据质数定理,1~n
中大约有 n l n n \frac{n}{ln n} lnnn个质数,假设有T
组数据,则计算量为 1 0 6 + T × 1 0 6 l n ( 1 0 6 ) ≈ 1 0 6 + 50000 × T 10^6 + T \times \frac{10^6}{ln(10^6)} \approx 10^6 + 50000 \times T 106+T×ln(106)106≈106+50000×T。完全可以接受。
代码
- C++
#include <iostream>
using namespace std;
const int N = 1000010;
int primes[N], cnt;
bool st[N];
void init(int n) {
for (int i = 2; i <= n; i++) {
if (!st[i]) primes[cnt++] = i;
for (int j = 0; primes[j] * i <= n; j++) {
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
int main() {
init(N - 1);
int n;
while (cin >> n, n) {
for (int i = 1; ; i++) { // primes[1] = 3, 从3开始
int a = primes[i];
int b = n - a;
if (!st[b]) {
printf("%d = %d + %d\n", n, a, b);
break;
}
}
}
return 0;
}
AcWing 1293. 夏洛克和他的女朋友
问题描述
分析
-
可以将这个题抽象成一个图,所有珠宝的价值是图中的顶点,当一个珠宝的价格是另一个珠宝的质因子时,我们可以在这两个点之间连接一条边。
-
如果这样建图的话,我们可以将质数放在左边,合数放在右边,则左边内部和右边内部是不可能有边的,边只在左右两边之间,因此此图是一个二分图,一个图是二分图,等价于该图可以二染色。
-
因此对于本题而言,当珠宝的数量小于等于2个时,只需要一种颜色即可,否则需要两种颜色;代码中我们不需要将图建立出来,我们只需要把
1~100000
中的质数全部筛出来,然后将所有的质数染成一种颜色,所有的合数染成另一种颜色即可。 -
时间复杂度是: O ( n ) O(n) O(n)。
代码
- C++
#include <iostream>
using namespace std;
const int N = 100010;
int n;
int primes[N], cnt;
bool st[N];
void init(int n) {
for (int i = 2; i <= n; i++) {
if (!st[i]) primes[cnt++] = i;
for (int j = 0; primes[j] * i <= n; j++) {
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
int main() {
cin >> n;
init(n + 1);
if (n <= 2) puts("1");
else puts("2");
for (int i = 2; i <= n + 1; i++)
if (!st[i]) printf("1 ");
else printf("2 ");
return 0;
}
AcWing 196. 质数距离
问题描述
-
问题链接:AcWing 196. 质数距离
分析
-
本题中要求求解在区间
[L, R]
之间相邻质数距离的最小值和最大值。 -
因为线性筛法只能从1开始筛,因此一种基本做法是将
1~R
中的所有质数筛出来,然后在区间[L, R]
中扫描一遍即可。但是这种做法对于本题不可行,因为L、R
最大为 2 31 − 1 = 2147483647 2^{31} - 1 = 2147483647 231−1=2147483647,二十多亿的计算量,空间也会超,不可行。 -
仔细读题可以发现 R − L ≤ 1 0 6 R - L \le 10 ^ 6 R−L≤106,我们可以考虑从这一点出发,考入如何只筛区间
[L, R]
,对于一个合数x
,则其一定存在一个质因子p
,且 p ≤ n p \le \sqrt n p≤n,因此我们只需要将 1 1 1~ 2 31 − 1 \sqrt {2 ^ {31} - 1} 231−1之间的质数筛出来,大于需要筛除1~50000
之间的质数即可。 -
则对于
[L, R]
之间的任意一个合数x
,则其必定存在一个质因子p
,其值在1~50000
之间且 p < x p < x p<x;反之也成立。即x
是合数 ⟺ \iff ⟺x
存在一个小于自己的质因子。 -
因此,本题的步骤是:
(1)找出
1~50000
中所有的质因子;(2)对于
1~50000
中的每个质数p
,将[L, R]
中的所有p
的倍数筛掉(至少是两倍);(3)遍历
[L, R]
之间的质数,找出距离最小值和最大值。 -
现在考虑第(2)步如何处理:如何枚举
[L, R]
中所有p
的倍数呢?我们只需要找到大于等于L
的最小的p
的倍数即可,该数值为 ⌈ L p ⌉ × p = ⌊ L + p − 1 p ⌋ × p \lceil \frac{L}{p} \rceil \times p = \lfloor \frac{L + p - 1}{p} \rfloor \times p ⌈pL⌉×p=⌊pL+p−1⌋×p。另外至少要是两倍,因此需要从 m a x ( p × 2 , ⌊ L + p − 1 p ⌋ × p ) max(p \times 2, \lfloor \frac{L + p - 1}{p} \rfloor \times p) max(p×2,⌊pL+p−1⌋×p)开始枚举。 -
时间复杂度:假设区间长度为 1 0 6 10^6 106,对于每个质数
p
,最多有 1 0 6 p \frac{10^6}{p} p106个p
的倍数,因此
1 0 6 2 + 1 0 6 3 + 1 0 6 5 + . . . = 1 0 6 × ( 1 2 + 1 3 + 1 5 + . . . ) = 1 0 6 × l o g ( l o g ( 1 0 6 ) ) \frac{10^6}{2} + \frac{10^6}{3} + \frac{10^6}{5} + ... = 10 ^ 6 \times (\frac{1}{2} + \frac{1}{3} + \frac{1}{5} + ...) = 10^6 \times log(log(\sqrt{10^6})) 2106+3106+5106+...=106×(21+31+51+...)=106×log(log(106))
即时间复杂度为 O ( n × l o g ( l o g ( n ) ) ) O(n \times log(log(\sqrt{n}))) O(n×log(log(n)))的。
代码
- C++
#include <iostream>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 1000010;
int primes[N], cnt; // 刚开始存储[1~50000]之间的质数, 然后被复用, 存储[L, R]之间的质数
int st[N]; // 也会被复用, 表示是否被筛掉
void init(int n) {
memset(st, 0, sizeof st);
cnt = 0;
for (int i = 2; i <= n; i++) {
if (!st[i]) primes[cnt++] = i;
for (int j = 0; primes[j] * i <= n; j++) {
st[i * primes[j]] = true;
if (i % primes[j] == 0) break;
}
}
}
int main() {
int l, r;
while (cin >> l >> r) {
// (1) 找出1~50000中所有的质因子
init(50000);
// (2) 对于1~50000中的每个质数p,将[L, R]中的所有p的倍数筛掉(至少是两倍)
memset(st, 0, sizeof st);
for (int i = 0; i < cnt; i++) {
LL p = primes[i];
for (LL j = max(p * 2, (l + p - 1) / p * p); j <= r; j += p)
st[j - l] = true; // 代表区间[L, R]中的数据j是合数
}
cnt = 0;
for (int i = 0; i <= r - l; i++)
if (!st[i] && i + l >= 2) // i+l = 1时,1不是质数
primes[cnt++] = i + l;
if (cnt < 2) puts("There are no adjacent primes.");
else {
int minp = 0, maxp = 0;
for (int i = 0 ; i + 1 < cnt; i++) {
int d = primes[i + 1] - primes[i];
if (d < primes[minp + 1] - primes[minp]) minp = i;
if (d > primes[maxp + 1] - primes[maxp]) maxp = i;
}
printf("%d,%d are closest, %d,%d are most distant.\n",
primes[minp], primes[minp + 1],
primes[maxp], primes[maxp + 1]);
}
}
return 0;
}
AcWing 197. 阶乘分解
问题描述
-
问题链接:AcWing 197. 阶乘分解
分析
-
阶乘的质因数分解: n ! n! n!所有的质因子一定是小于等于n的,否则如果存在大于n的质因子,说明该质因子一定是某几个数相乘得到的,违反质数的定义。
(1)求出1~n中所有的质数,可以使用线性法筛质数;
(2)枚举某个质数p,则其在n!的质因数分解中出现的次数为:
⌊ n p ⌋ + ⌊ n p 2 ⌋ + . . . . . . \lfloor \frac{n}{p} \rfloor + \lfloor \frac{n}{p^2} \rfloor + ...... ⌊pn⌋+⌊p2n⌋+......
该方法求阶乘 n ! n! n!的质因数分解的时间复杂度大约为 O ( n ) O(n) O(n),因为1~n中大约有 n l o g ( n ) \frac{n}{log(n)} log(n)n个质数,求每个质数出现次数计算大约为 l o g ( n ) log(n) log(n)的,相乘得到阶乘质因数分解时间复杂度大约为 O ( n ) O(n) O(n)的。 -
另外指的一提的是:AcWing 888. 求组合数 IV这一题中就用到了阶乘的质因数分解。
代码
- C++
#include <iostream>
using namespace std;
const int N = 1000010;
int primes[N], cnt;
bool st[N];
void get_primes(int n) {
for (int i = 2; i <= n; i++) {
if (!st[i]) primes[cnt++] = i;
for (int j = 0; primes[j] <= n / i; j++) {
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
int main() {
int n;
scanf("%d", &n);
get_primes(n);
for (int i = 0; i < cnt; i++) {
int p = primes[i];
int s = 0, t = n;
while (t) s += t / p, t /= p;
printf("%d %d\n", p, s);
}
return 0;
}
3. 力扣上的质数题目
Leetcode 0204 计数质数
问题描述
-
问题链接:Leetcode 0204 计数质数
分析
- 上述原理中筛质数的三种方法随便选一种即可。
代码
- C++
// 线性筛法
/**
* 执行用时:80 ms, 在所有 C++ 提交中击败了67.75%的用户
* 内存消耗:9 MB, 在所有 C++ 提交中击败了45.33%的用户
*/
class Solution {
public:
int countPrimes(int n) {
n--;
vector<bool> st(n); // i为合数的话st[i]=true
vector<int> p;
for (int i = 2; i <= n; i++) {
if (!st[i]) p.push_back(i);
for (int j = 0; p[j] <= n / i; j++) {
st[i * p[j]] = true;
if (i % p[j] == 0) break;
}
}
return p.size();
}
};
- Java
/**
* Date: 2020/10/4 8:55
* Content: 线性筛法,时间复杂度:O(n)
* 执行用时:37 ms, 在所有 Java 提交中击败了39.25%的用户
* 内存消耗:45.1 MB, 在所有 Java 提交中击败了7.91%的用户
*/
public class Solution {
public int countPrimes(int n) {
n--;
boolean[] st = new boolean[n + 5];
Arrays.fill(st, false);
int[] p = new int[n + 5];
int cnt = 0;
for (int i = 2; i <= n; i++) {
if (!st[i]) p[cnt++] = i;
for (int j = 0; p[j] <= n / i; j++) {
st[p[j] * i] = true;
if (i % p[j] == 0) break;
}
}
return cnt;
}
}