素数

在算法竞赛,数论是绕不开的话题,而素数(或称质数)则是数论中最基础也最重要的核心概念之一。无论是直接考察素数性质,还是作为其他数论算法(如欧拉函数、模运算、因数分解)的基础,对素数深刻的理解和高效的算法掌握都是冲击奖牌的关键。
1. 素数基础 (Prime Basics)
1.1 定义
一个大于 1 的正整数
1.2 试除法判定素数 (Trial Division Primality Test)
这是最直观的素数判定方法。要判断一个整数
优化: 我们注意到,如果
因此,我们只需要检查
代码实现:
1 |
|
复杂度分析:
- 时间复杂度:
。对于单次判定,这个效率在 较大时(例如 或更大)是无法接受的。 - 空间复杂度:
。
局限性: 当需要判断大量数字,或者数字本身非常大时,试除法效率低下。我们需要更快的工具。
2. 素数筛法 (Sieving for Primes)
当我们需要找出一定范围内的所有素数时(例如
2.1 埃拉托斯特尼筛法 (Sieve of Eratosthenes)
这是最古老、最经典的素数筛法。其基本思想是:从 2 开始,将每个素数的倍数都标记为合数。
步骤:
- 创建一个布尔数组
isp[]
(或vis[]
,表示 visited),大小为,初始化所有元素为 true
(或false
,看标记方式),表示所有数初始时都“可能是素数”。isp[0]
和isp[1]
标记为false
。 - 从
开始遍历到 : - 如果
isp[i]
为true
,说明是一个素数。 - 将
的所有倍数 (不超过 )标记为合数,即设置 isp[j] = false
。
- 如果
- 遍历完成后,所有
isp[i]
仍为true
的就是范围内的素数。
优化:
- 标记倍数时,可以从
开始。因为对于 , 这个合数一定在处理 时被 的某个素因子筛掉了。 - 外层循环只需要遍历到
即可标记所有合数,但为了得到所有素数列表,通常还是遍历到 。
代码实现:
1 |
|
复杂度分析:
- 时间复杂度:
。直观上看是 。根据素数定理的推论,素数的倒数和近似于 。这是一个非常接近线性的复杂度,对于 在 甚至 级别都非常高效。 - 空间复杂度:
,主要用于存储 isp
数组。
2.2 线性筛 (Linear Sieve / Euler’s Sieve)
埃氏筛法虽然高效,但仍有冗余:一个合数会被它的多个素因子重复标记。例如,12 会被 2 标记一次,也会被 3 标记一次。线性筛的目标是让每个合数只被其 最小的素因子 标记一次,从而达到严格的
核心思想:
维持一个当前找到的素数列表 prm
。遍历
- 如果
没有被标记过,则 是素数,将其加入 prm
列表。 - 对于
prm
中的每个素数: - 标记
为合数。 - 关键: 如果
,则停止用 prm
中更大的素数去标记的倍数。 - 原因: 如果
是 的倍数,即 ,那么对于 prm
中下一个更大的素数,合数 。这个合数的最小素因子显然是 (因为 且 是 的因子,所以也是 的因子)。根据线性筛的原则(每个合数只被其最小素因子筛一次), 应该在后面遍历到 时,被 筛掉,而不是现在被 筛掉。因此,一旦 是 的倍数,就必须 break
,保证是被其最小素因子 筛掉的。
- 原因: 如果
- 标记
代码实现:
1 |
|
复杂度分析:
- 时间复杂度:
。每个合数 只会被其最小素因子 在 时标记一次。外层循环 次,内层 for
循环的break
条件保证了每个合数只被访问一次。 - 空间复杂度:
,用于存储 isp
数组和prm
数组(素数个数约为)。
应用场景:
线性筛是算法竞赛中预处理素数的最常用方法,尤其适用于需要
3. 质因数分解 (Prime Factorization)
算术基本定理 (Fundamental Theorem of Arithmetic): 任何一个大于 1 的正整数
其中
质因数分解是数论问题的基石之一。
3.1 试除法分解 (Trial Division Factorization)
最朴素的方法是试除法。我们尝试用从 2 开始的整数去除
步骤:
- 遍历
从 2 开始。 - 如果
能整除 (即 ): 是 的一个质因子。记录 。 - 将
中所有的因子 都除尽,即 直到 不能再被 整除。同时记录 的指数。
- 继续增加
,重复步骤 2。 - 优化: 和素数判定类似,我们只需要尝试
到 即可。当循环结束时,如果 ,那么剩下的 本身一定是一个大于 的素因子。 - 证明: 假设循环结束后
且 是合数,那么 必有一个小于等于 的素因子 。由于我们是从小到大尝试除数的,这个素因子 在之前的循环中一定会被尝试。如果 ,那么在循环到 时, 就会被 除尽,或者至少 的值会变小。如果 ,但仍然 (其中 是循环到 之前的 值), 也会被除掉。只有当 在循环结束后剩下的值是一个大于 的素数时,才不会在 的循环中被除掉。
- 证明: 假设循环结束后
代码实现:
1 |
|
复杂度分析:
- 时间复杂度:
。对于单个数的分解,这个效率在 达到 左右是可接受的,但对于 级别的 则不够快。 - 空间复杂度:
或 ,其中 是 的不同质因子个数。空间主要用于存储结果,因子个数通常不多。
优化: 如果需要对多个数进行质因数分解,且这些数范围不大(例如都在
3.2 Pollard’s Rho 算法 (进阶)
对于非常大的数(例如
核心思想:
该算法试图利用随机函数在模
它使用一个伪随机序列
- 如果
,说明这次尝试失败(可能选取的 或初始值 不好,或者 本身是素数),需要更换 或 重试。 - 如果
,则找到了一个因子 。我们可以递归地对 和 进行分解。
注意: Pollard’s Rho 是随机算法,不能保证一定能快速找到因子,但在实践中对于
复杂度: 启发式期望时间复杂度约为
代码实现: 由于涉及较多的细节(大数乘法取模、GCD、随机化、递归),Pollard’s Rho 的代码相对复杂。在 ICPC 竞赛中,如果不是专门准备数论难题的队伍,可能不会现场实现它,但了解其存在和适用场景是有益的。对于冲击金牌的选手,掌握 Pollard’s Rho 是必要的。
(注:此处不提供 Pollard’s Rho 的完整代码,因为它较为复杂且超出初学者/提高组的常规要求,其实现需要高效的模乘、模幂和 GCD 算法。)
4. 模运算与数论定理 (Modular Arithmetic & Theorems)
模运算是数论的基石,在算法竞赛中无处不在。素数在模运算中扮演着特殊的角色。
4.1 费马小定理 (Fermat’s Little Theorem)
定理叙述: 如果
推论: 对于任意整数
应用:
- 快速计算模幂的逆元: 当模数
是素数时,且 ,我们可以用费马小定理计算 在模 下的乘法逆元。因为 ,所以 。因此, 就是 的模 逆元。这需要配合快速幂算法(见下文)来高效计算 。 - 概率性素数测试 (Fermat Primality Test): 如果对于一个数
,我们随机选取几个 (满足 ),计算 。如果结果不为 1,则 必定是合数。如果对于所有选取的 结果都为 1,则 可能是素数。然而,存在一些合数(称为 Carmichael 数,例如 561)使得对于所有与其互质的 ,都有 ,因此费马测试是不可靠的,但它是更强的 Miller-Rabin 测试的基础。
快速幂 (Modular Exponentiation):
用于在
1 |
|
注意: 在计算 res = res * a % m
和 a = a * a % m
时,如果 long long
级别(例如 long long
的范围(约 __int128
来处理中间结果,这是 g++/clang 的一个扩展类型,可以存储更大的整数。如果编译器不支持 __int128
,则需要使用更复杂的“龟速乘”(二进制乘法)或者基于 long double
的近似技巧来避免溢出。对于标准 ICPC 规则,通常允许使用 __int128
。
快速幂复杂度:
4.2 欧拉定理 (Euler’s Theorem)
费马小定理是欧拉定理在模数为素数时的特例。欧拉定理适用于模数为任意正整数的情况。
欧拉函数 (Euler’s Totient Function)
计算
如果
展开后也可以写成:
性质:
- 如果
是素数, 。 - 如果
是素数, ,则 。 - 欧拉函数是 积性函数:如果
,则 。
欧拉定理叙述: 如果整数
扩展欧拉定理 (或称欧拉降幂公式):
对于任意整数
注意: 这个扩展定理 不要求
应用:
- 计算模幂逆元 (通用情况): 如果
,则 模 的逆元是 。这需要先计算 。 - 指数降幂: 在计算
时,如果指数 非常大,可以使用欧拉定理(或扩展欧拉定理)将指数 对 (或在某些情况下是 再加 )取模,从而减小指数的规模。这对于处理形如 的问题非常有用。
计算
单个计算: 利用质因数分解公式。先对
进行质因数分解 ,然后套用公式 。时间复杂度主要取决于质因数分解,即 。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using namespace std;
using ll = long long;
// 功能:计算 phi(n) (基于质因数分解)
// 参数:n - 正整数
// 返回值:phi(n)
ll phi(ll n) {
if (n == 0) return 0; // phi(0) 无定义或视情况处理
ll res = n;
for (ll i = 2; i * i <= n; ++i) {
if (n % i == 0) {
res = res / i * (i - 1); // 等价于 res *= (1 - 1/i)
while (n % i == 0) {
n /= i;
}
}
}
if (n > 1) { // 如果最后剩下一个大于 sqrt(n_original) 的质因子
res = res / n * (n - 1);
}
return res;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
ll n;
cin >> n;
cout << "phi(" << n << ") = " << phi(n) << endl;
return 0;
}复杂度:
。 批量计算 (线性筛): 可以在线性筛的同时计算出
到 所有数的欧拉函数值。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
using namespace std;
using ll = long long;
const int N = 1e7 + 5;
bool isp[N];
int prm[N / 10];
int cnt = 0;
int phi_val[N]; // 存储 phi 值
// 功能:线性筛计算 1 到 n 的 phi 值
// 参数:n - 上限
void sieve_phi(int n) {
fill(isp, isp + n + 1, false);
isp[0] = isp[1] = true;
phi_val[1] = 1; // phi(1) = 1
cnt = 0;
for (int i = 2; i <= n; ++i) {
if (!isp[i]) { // i 是素数
prm[cnt++] = i;
phi_val[i] = i - 1; // phi(p) = p - 1
}
for (int j = 0; j < cnt && (ll)i * prm[j] <= n; ++j) {
int pj = prm[j];
isp[i * pj] = true;
if (i % pj == 0) { // pj 是 i 的最小质因子
// phi(i * pj) = phi(i) * pj
// 因为 i 包含了 pj, i*pj 与 i 相比,只是 pj 的指数增加了 1
// 根据 phi(p^k) = p^(k-1)(p-1) 推导
phi_val[i * pj] = phi_val[i] * pj;
break;
} else { // pj 不是 i 的最小质因子 (即 gcd(i, pj) = 1)
// phi 是积性函数: phi(i * pj) = phi(i) * phi(pj)
phi_val[i * pj] = phi_val[i] * (pj - 1); // phi(pj) = pj - 1
}
}
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int n;
cin >> n;
sieve_phi(n);
// 打印前 10 个 phi 值
for (int i = 1; i <= min(n, 10); ++i) {
cout << "phi(" << i << ") = " << phi_val[i] << "\n";
}
return 0;
}复杂度:
时间复杂度, 空间复杂度。
4.3 模逆元 (Modular Inverse)
给定整数
存在条件:
求解方法:
费马小定理: 当
是素数且 时,逆元为 。需要 的快速幂。 欧拉定理: 当
时,逆元为 。需要计算 (若单次计算是 或 预处理 )和快速幂 。 扩展欧几里得算法 (Extended Euclidean Algorithm, ExGCD):
这是求解模逆元最通用和常用的方法,因为它不要求是素数,只需要 。
扩展欧几里得算法用于求解方程的一组整数解 。
如果,那么方程变为 。对这个方程两边同时取模 ,得到 。因此,ExGCD 算出的 (可能需要调整到 范围内) 就是 模 的逆元。 代码实现 (ExGCD):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
using namespace std;
using ll = long long;
// 功能:扩展欧几里得算法,求解 ax + by = gcd(a, b)
// 参数:a, b - 输入整数
// x, y - 引用参数,返回方程的一组特解
// 返回值:gcd(a, b)
ll exgcd(ll a, ll b, ll &x, ll &y) {
if (b == 0) {
x = 1;
y = 0;
return a;
}
ll d = exgcd(b, a % b, y, x); // 注意 x, y 的位置交换
y -= (a / b) * x;
return d;
}
// 功能:计算 a 模 n 的逆元 (使用 ExGCD)
// 要求:gcd(a, n) = 1
// 返回值:a 的模 n 逆元,若不存在则返回 -1 (或其他错误标记)
ll mod_inv_exgcd(ll a, ll n) {
ll x, y;
ll d = exgcd(a, n, x, y);
if (d != 1) {
return -1; // 逆元不存在
}
// x 可能是负数,调整到 [0, n-1] 范围
return (x % n + n) % n;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
ll a, n;
// 示例:计算 3 模 7 的逆元
a = 3; n = 7;
ll inv = mod_inv_exgcd(a, n);
if (inv != -1) {
cout << "Inverse of " << a << " mod " << n << " is " << inv << endl; // 5
} else {
cout << "Inverse does not exist." << endl;
}
// 示例:计算 6 模 9 的逆元
a = 6; n = 9; // gcd(6, 9) = 3 != 1
inv = mod_inv_exgcd(a, n);
if (inv != -1) {
cout << "Inverse of " << a << " mod " << n << " is " << inv << endl;
} else {
cout << "Inverse does not exist." << endl; // 不存在
}
return 0;
}复杂度: 扩展欧几里得算法的时间复杂度与欧几里得算法相同,为
或 。
选择哪种方法?
- 如果模数
是素数,费马小定理最简单直接。 - 如果模数
不是素数,或者需要处理 是素数和合数混合的情况,扩展欧几里得算法是标准做法。 - 欧拉定理法需要计算
,通常不如 ExGCD 直接。但在某些需要预计算 值的场景下也可行。
5. 大素数判定 (Large Primality Testing)
当需要判断的数
5.1 Miller-Rabin 算法
Miller-Rabin 是目前在算法竞赛中最常用的大数素性测试算法。它是一种 随机化算法,能够在
核心思想:
基于费马小定理和二次探测定理。
- 二次探测定理: 如果
是一个素数,且 是一个整数使得 ,则方程 的解只有 或 。 - 推论: 如果找到一个
使得 但 且 ,则 一定是合数。
- 推论: 如果找到一个
算法步骤:
- 处理小情况: 如果
,则 不是素数。如果 或 ,则 是素数。如果 是偶数且 ,则 是合数。 - 分解
: 将 表示为 ,其中 是奇数, 。 - 进行
轮测试: - 随机选择一个整数
,满足 。(实践中通常选择一些小的素数作为基底 ,例如 {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37}。对于 的范围,使用特定的几组基底就能做到 100% 确定性判断)。 - 计算
。 - 如果
或 ,则 可能是素数,继续下一轮测试(或换一个基底 )。 - 否则,进行
次平方探测: - 重复
次:令 。 - 如果在某次平方后
,则 可能是素数,跳出内层循环,继续下一轮测试。 - 如果在某次平方后
(但之前的 不是 ),根据二次探测定理, 是合数,直接返回 false
。
- 重复
- 如果
次循环(包括初始的 )结束后, 仍然不等于 ,则说明费马小定理 不成立(或者在中间步骤违反了二次探测),因此 是合数,返回 false
。
- 随机选择一个整数
- 通过所有测试: 如果
通过了 轮测试(对于所有选定的基底 ),则我们 高度确信 是素数,返回 true
。
确定性基底: 对于
代码实现 (Miller-Rabin):
1 |
|
复杂度分析:
- 时间复杂度: 一次
miller_rabin_check
的复杂度主要是快速幂和平方探测,约为(如果认为模乘是 )或 (如果认为模乘是 或 ,这取决于实现,通常视为 或更快)。进行 轮测试,总复杂度为 或 。使用确定性基底时 是常数。 - 空间复杂度:
(不计 __int128
的开销)。
适用场景: 判断
6. 实战
- 整数溢出: 在处理大数时,尤其是乘法和快速幂中,中间结果很容易超过
long long
的范围。务必使用__int128
(如果可用) 或实现防溢出模乘(龟速乘)。 - 选择合适的算法:
- 判断单个小整数(如
)是否为素数:可以直接用预处理的筛法 isp[]
数组查询。 - 判断单个中等整数(如
)是否为素数:试除法 可能可行。 - 判断单个大整数(如
或更大):必须用 Miller-Rabin 。 - 找出
到 ( 或 ) 的所有素数:线性筛 是最佳选择。埃氏筛 也可以。 - 质因数分解小整数(
):可以结合筛法预处理素数,再用这些素数试除,复杂度仍是 或更快(如果因子都很小)。 - 质因数分解中等整数(
):试除法 。 - 质因数分解大整数(
):先用 Miller-Rabin 判素,如果是合数,用 Pollard’s Rho 尝试分解。
- 判断单个小整数(如
- 边界条件: 注意处理
等特殊情况。负数通常不讨论素性,但如果题目涉及,需明确定义或按题意处理。 - 复杂度意识: 时刻关注算法的时间和空间复杂度是否满足题目限制。尤其是在循环嵌套或递归中。
- 模板化: 将筛法、快速幂、ExGCD、Miller-Rabin 等常用数论算法写成可靠的模板,比赛时可以快速调用。
- 组合使用: 很多数论题需要结合多种技术,例如先筛素数,再用素数进行分解或计算欧拉函数,最后结合模运算定理求解。
7. 总结
素数是数论世界的基础构件,掌握素数相关的核心概念和高效算法是必备技能。
- 基础: 理解素数定义,掌握
的试除法判定和分解。 - 核心: 精通埃氏筛
和线性筛 ,能在 内预处理素数及相关积性函数(如 )。 - 进阶: 熟练运用费马小定理、欧拉定理进行模逆元计算和指数降幂,掌握
的快速幂和扩展欧几里得算法。 - 高阶: 掌握
的 Miller-Rabin 大数素性测试,了解 的 Pollard’s Rho 大数分解算法。 - 实战: 注意数据范围、整数溢出、边界条件,根据问题规模选择最合适的算法,并能够灵活组合运用这些工具。
- Title: 素数
- Author: YZY
- Created at : 2025-07-02 02:23:23
- Updated at : 2025-07-02 02:23:23
- Link: https://blog.dtoj.team/2025/07/02/素数/
- License: 三金家的Y同学