前言-揭开质数的面纱
在数学的广阔天地中,质数就像是一颗颗璀璨的明珠,散落在数轴的各个角落。它们是最基本的算术元素,拥有着独特的性质和魅力,自古以来就吸引了无数数学家的目光。素数不仅在数学领域内具有重要地位,而且在现代密码学、计算机科学等多个领域都有着广泛的应用。
什么是质数?
质数,又称素数,是指在大于 1 1 1 的自然数中,除了 1 1 1 和它本身以外不再有其他因数的数。例如, 2 2 2、 3 3 3、 5 5 5、 7 7 7、 11 11 11 等都是质数。
值得注意的是, 1 1 1 不是质数,因为它只有一个因数。
质数有两个基本的性质:
- 任何大于 1 1 1 的自然数都可以分解为几个质数的乘积,这种分解称为质因数分解。
- 质数有无限多个。这是数学上一个非常著名的定理,由古希腊数学家欧几里得在公元前 300 300 300 年左右首次证明。
质数在数论中扮演着重要的角色,许多数学定理都与质数相关。
PS:
质数是数学中的一个重要概念,与之相关的数学定理非常多。以下是一些著名的与质数相关的数学定理:
- 欧几里得定理(Euclid’s Theorem):定理证明了质数有无限多个。它表明,对于任何给定的质数,总存在一个比它更大的质数。
- 算术基本定理(Fundamental Theorem of Arithmetic):这个定理表明,每个大于 1 1 1的自然数都可以唯一地表示为几个质数的乘积,这个表示称为质因数分解。
- 唯一分解定理(Unique Factorization Theorem):在整数范围内,每个大于 1 1 1的整数都可以写成唯一的质因数乘积,即它的标准分解式是唯一的。
- 贝祖定理(Bezout’s Identity):此定理与数论中的最大公约数有关,但它也适用于质数,表明任意两个整数 a a a和 b b b,它们的最大公约数可以表示为 a a a和 b b b的线性组合。
- 费马小定理(Fermat’s Little Theorem):如果 p p p是一个质数,那么对于任何整数 a a a, a p − a a^p - a ap−a都是 p p p的倍数。
- 欧拉定理(Euler’s Theorem):这个定理是费马小定理的推广,它表明如果 n n n和 a a a是互质的正整数,那么 a φ ( n ) ≡ 1 ( m o d n ) a^φ(n) ≡ 1 (mod n) aφ(n)≡1(modn),其中 φ ( n ) φ(n) φ(n)是欧拉函数。
- 素数定理(Prime Number Theorem):这个定理描述了质数在自然数中的分布规律,表明小于或等于某个数 n n n的质数的数量大约是 n / l n ( n ) n / ln(n) n/ln(n)。
- 孪生素数猜想(Twin Prime Conjecture):这个猜想(尚未证明为定理)表明存在无限多对孪生素数,即相差为 2 2 2的质数对。
什么是合数?
合数是指一个大于 1 1 1的自然数,它除了 1 1 1和它本身作为因数外,还有其他的因数。换句话说,一个合数可以被分解为两个或两个以上小于它本身的正整数的乘积。
例如, 4 4 4、 6 6 6、 8 8 8、 9 9 9、 10 10 10等都是合数,因为它们都可以表示为两个更小的正整数的乘积( 4 = 2 × 2 4 = 2 × 2 4=2×2, 6 = 2 × 3 6 = 2 × 3 6=2×3, 8 = 2 × 4 8 = 2 × 4 8=2×4, 9 = 3 × 3 9 = 3 × 3 9=3×3, 10 = 2 × 5 10 = 2 × 5 10=2×5)。
合数与质数的关系
-
定义上的互补:素数和合数是大于 1 1 1的自然数中的两个互补的概念。一个数如果不是素数,那么它就是合数;反之亦然。
-
质因数分解:根据算术基本定理,每个合数都可以唯一地分解为几个素数的乘积,这种分解称为质因数分解。例如,合数 18 18 18可以分解为 2 × 3 × 3 2 × 3 × 3 2×3×3,其中 2 2 2和 3 3 3都是素数。
-
最小素因数:合数至少有一个素数作为它的因数,这个最小的素因数称为合数的最小素因数。
-
最大公约数:任意两个合数的最大公约数(GCD)要么是 1 1 1,要么是一个合数。这是因为最大公约数是由两个数的共有质因数构成的,而这些质因数也是合数的因数。
-
合数的性质:合数的性质通常与其质因数分解有关。例如,一个合数的因数个数可以通过其质因数分解来确定。
质数个数
质数个数统计函数:小于等于 x x x 的质数个数,一般用$\pi(x)\approx \frac{x}{ln(x)} $。
试除法判断质数
用于判断一个给定的整数是否是质数。
过程
-
特殊情况处理:首先检查 n n n是否小于 2 2 2,因为小于 2 2 2的数都不是质数。如果 n n n等于2,它是质数。如果 n n n是偶数且大于 2 2 2,它不是质数。
-
选择试除数:从 2 2 2开始,选择一个试除数 i i i(初始时 i = 2 i=2 i=2),它应该小于或等于 √ n √n √n(因为如果 n n n有一个因子大于 √ n √n √n,那么它必定还有一个因子小于或等于 √ n √n √n)。
-
试除:检查 n n n是否能被 i i i整除(即 n n % i == 0 n)。如果 n n n能被 i i i整除,那么 n n n不是质数,因为 i i i是 n n n的一个因子。
-
迭代:如果 n n n不能被 i i i整除,将 i i i增加 1 1 1,然后重复步骤 3 3 3。
-
终止条件:如果 i i i超过了 √ n √n √n,且在这个过程中没有找到能整除 n n n的数,则 n n n是质数。
试除法的代码:
bool is_prime(int x) // 判定质数
{
if (x < 2) return false;
for (int i = 2; i <= x / i; i ++ )
if (x % i == 0)
return false;
return true;
}
优缺点
- 优点:简单易懂,易于实现。
- 缺点:对于大数效率低下,因为需要检查大量的除数。随着n的增大,所需的时间会呈指数级增长。
分解质因数
分解质因数,也称为质因数分解,是将一个合数分解为若干个质数乘积的过程。这些质数称为原合数的质因数。
原理
质因数分解基于算术基本定理,即每个大于 1 1 1的自然数都可以唯一地表示为几个质数的乘积,这个表示称为该数的质因数分解。
-
找到最小的质因数:从最小的质数 2 2 2开始,检查原数是否能被它整除。如果能,那么 2 2 2是最小的质因数,然后除以 2 2 2,继续检查除以 2 2 2后的结果。
-
检查奇数质因数:当原数不能再被 2 2 2整除时,从 3 3 3开始,以 2 2 2为步长检查奇数是否能整除原数。
-
继续分解:每次找到一个质因数后,除以这个质因数,并对得到的结果继续进行质因数分解,直到结果为 1 1 1。
-
终止条件:当分解到结果为 1 1 1时,所有的质因数都已被找到。
优缺点
优点:
- 唯一性:根据算术基本定理,质因数分解的结果是唯一的,这为解决某些数学问题提供了便利。
缺点:
- 计算难度:对于较大的数,质因数分解是一个计算上非常困难的问题。目前没有已知的多项式时间算法能够解决所有合数的质因数分解问题。
分解质因数的代码
std::vector<int> primeFactorization(int n) {
std::vector<int> factors;
if (n <= 1) return factors;
while (n % 2 == 0) {
factors.push_back(2);
n /= 2;
}
for (int i = 3; i <= sqrt(n); i += 2) {
while (n % i == 0) {
factors.push_back(i);
n /= i;
}
}
if (n > 2) {
factors.push_back(n);
}
return factors;
}
埃氏筛质数
背景
埃拉托斯特尼筛法最早由古希腊数学家埃拉托斯特尼(Eratosthenes of Cyrene)提出,他提出这个筛法是为了找出所有小于或等于给定数的质数。
基本原理
埃氏筛法的基本原理是通过逐步筛除合数来找到所有的质数。
- 创建一个列表,包含从 2 2 2开始到给定数 n n n的所有整数。
- 选择列表中的第一个数(它是质数),然后筛除它的所有倍数(这些倍数不可能是质数)。
- 选择列表中的下一个未被筛除的数,它也是质数,然后筛除它的所有倍数。
- 重复步骤 3 3 3,直到没有更多的数可以选择。
- 列表中未被筛除的数都是质数。
埃氏筛质数代码一
std::vector<int> get_primes(int n) {
std::vector<bool> prime(n + 1, true); // 如果对应的 i 是true,则表明 i 是质数
std::vector<int> primes; // 存储所有的 1 ~ n 的质数
prime[0] = prime[1] = false;
for (int p = 2; p * p <= n; p++) {
if (prime[p] == true) {
for (int i = p * p; i <= n; i += p)
prime[i] = false;
}
}
for (int p = 2; p <= n; p++) {
if (prime[p]) {
primes.push_back(p);
}
}
return primes;
}
埃氏筛质数代码二
std::vector<int> get_primes(int n) {
std::vector<bool> st(n); // 判断当前的数字 i 是否被筛选过
std::vector<int> primes(n); // 存储所有的 1 ~ n 的质数
int cnt = 0;
for (int i = 2; i <= n; i ++) {
if(!st[i]) {
primes[cnt ++] = i;
for (int j = 2 * i; j <= n; j += i)
st[j] = 1;
}
}
return primes;
}
优缺点
算法的不足
- 埃氏筛法需要一个能够存储从 2 2 2到 n n n所有整数的数组,因此它的空间复杂度较高,对于非常大的 n n n,这可能导致内存不足的问题。
- 虽然埃氏筛法的时间复杂度是 O ( n l o g l o g n ) O(n log log n) O(nloglogn),但对于非常大的 n n n,算法的常数因子可能会使得实际运行时间较长。
- 埃氏筛法适用于找出小于等于某个不是特别大的数 n n n的所有质数。
优点
- 埃氏筛法的原理和实现都非常简单,易于理解和编程实现。
- 对于不是特别大的 n n n,埃氏筛法是一种非常有效的找出所有质数的方法。
欧拉筛质数
欧拉筛法,也称为线性筛法,是由数学家欧拉提出的一种用于筛选质数的算法。
背景
欧拉筛法是针对埃拉托斯特尼筛法的优化。埃拉托斯特尼筛法在筛选质数时,对于每个质数 p p p,会标记掉所有 p p p的倍数,包括那些已经被更小质数标记过的数。欧拉筛法通过避免重复标记来提高效率。
原理
欧拉筛法的核心思想是对于每个合数 n n n,只将其标记为它的最小质因数的倍数。算法维护一个当前已知的质数列表,并遍历每个整数 i i i,对于每个质数 p p p( p p p是 i i i的倍数),如果 i i i未被标记,则 i i i是质数,将其加入质数列表;否则,将 i i i标记为合数,并将其与 p p p的最小公倍数关联起来。
算法步骤
- 初始化一个布尔数组,表示每个数字是否为质数。
- 创建一个列表来存储已找到的质数。
- 遍历每个整数 i i i,对于每个质数 p p p( p p p是 i i i的倍数且 p < = s q r t ( i ) p <= sqrt(i) p<=sqrt(i)),如果 i i i未被标记为合数,则标记 i i i为 p p p的倍数。
- 如果 i i i未被标记为任何质数的倍数,则 i i i是质数,将其加入质数列表。
欧拉筛法代码一:
std::vector<int> getrimes(int n) {
std::vector<bool> isPrime(n + 1, true); // 如果对应的 i 是true,则表明 i 是质数
std::vector<int> primes; // 存储所有的 1 ~ n 的质数
isPrime[0] = isPrime[1] = false;
for (int i = 2; i <= n; ++i) {
if (isPrime[i]) {
primes.push_back(i);
}
for (int j = 0; j < primes.size() && i * primes[j] <= n; ++j) {
isPrime[i * primes[j]] = false;
if (i % primes[j] == 0) break; // 只被最小的质数筛去
}
}
return primes;
}
欧拉筛法代码二:
std::vector<int> get_primes(int n) {
std::vector<int> primes(n); // 存储所有的 1 ~ n 的质数
std::vector<bool> st(n); // 判断当前的数字 i 是否被筛选过
int cnt = 0;
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] = 1;
if(!(i % primes[j])) break; // 只被最小的质数筛去
}
}
return primes;
}
优缺点
优点:
- 效率高:与埃拉托斯特尼筛法相比,欧拉筛法减少了重复标记的次数,因此在筛选大范围内的质数时更加高效。
- 线性时间复杂度:理论上,欧拉筛法的时间复杂度接近于O(n),这使得它在处理大规模数据时非常有用。
缺点:
- 实现复杂:相较于埃拉托斯特尼筛法,欧拉筛法的实现更为复杂,需要额外的数据结构来存储已知的质数。
- 对于小规模数据效果不明显:在小规模数据集上,欧拉筛法的优势不明显,甚至可能比埃拉托斯特尼筛法慢。
洛谷 P3383 题目描述
如题,给定一个范围 n n n,有 q q q 个询问,每次输出第 k k k 小的素数。
输入格式
第一行包含两个正整数 n , q n,q n,q,分别表示查询的范围和查询的个数。
接下来 q q q 行每行一个正整数 k k k,表示查询第 k k k 小的素数。
输出格式
输出 q q q 行,每行一个正整数表示答案。
样例 #1
样例输入 #1
100 5
1
2
3
4
5
样例输出 #1
2
3
5
7
11
提示
【数据范围】
对于
100
%
100\%
100% 的数据,
n
=
1
0
8
n = 10^8
n=108,
1
≤
q
≤
1
0
6
1 \le q \le 10^6
1≤q≤106,保证查询的素数不大于
n
n
n。
代码
#include <iostream>
#include <vector>
using namespace std;
std::vector<int> get_primes(int n) {
std::vector<int> primes(n);
std::vector<bool> st(n);
int cnt = 0;
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] = 1;
if(!(i % primes[j])) break;
}
}
return primes;
}
int main() {
int n, q; cin >> n >> q;
std::vector<int> ans = get_primes(n);
while(q --) {
int num; cin >> num;
cout << ans[num - 1] << endl;
}
return 0;
}