news 2026/4/18 11:29:34

【算法基础篇】(三十九)数论之从质数判定到高效筛法:质数相关核心技能全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【算法基础篇】(三十九)数论之从质数判定到高效筛法:质数相关核心技能全解析


目录

​编辑

前言

一、质数的定义与直观判定

1.1 质数与合数的概念

1.2 试除法的优化:从 O (n) 到 O (√n)

1.3 C++ 实现质数判定函数

1.4 实战例题:洛谷 P5736 质数筛

二、筛法入门:埃氏筛法(Eratosthenes Sieve)

2.1 筛法的核心思想

2.2 埃氏筛法的基本实现

2.3 C++ 实现埃氏筛法

2.4 埃氏筛法的时间复杂度与优化

2.5 实战例题:洛谷 P3383 模板线性筛素数

三、筛法巅峰:线性筛法(欧拉筛法)

3.1 埃氏筛法的局限性

3.2 线性筛法的核心原理

3.3 线性筛法的逻辑拆解

3.4 C++ 实现线性筛法

3.5 线性筛法的优势与应用场景

四、进阶实战:质数相关经典例题解析

4.1 例题 1:洛谷 P1835 素数密度

4.2 例题 2:UVA543 Goldbach's Conjecture

五、质数相关算法的常见误区与优化技巧

5.1 常见误区

5.2 优化技巧

总结


前言

在算法竞赛的数论板块中,质数相关问题始终是高频考点。无论是判断单个数字是否为质数,还是快速筛选出一定范围内的所有质数,这些基础技能不仅是解决复杂数论问题的基石,更是拉开竞赛分数差距的关键。本文将从质数的定义出发,深入剖析质数判定的优化思路,详解埃氏筛法与线性筛法的原理与实现,带大家彻底掌握这部分核心内容。下面就让我们正式开始吧!


一、质数的定义与直观判定

1.1 质数与合数的概念

质数(又称素数)是指大于 1 的自然数中,除了 1 和它本身以外不再有其他因数的数。与之相对的是合数,合数是指大于 1 且不是质数的自然数。特别规定:1 既不是质数也不是合数。

这个定义看似简单,但在实际判定时却藏着不少优化空间。比如判断一个数 x 是否为质数,最直观的思路是检查从 2 到 x-1 的所有整数是否能整除 x。但这样的暴力解法效率极低,当 x 达到 1e5 甚至更大时,必然会超时。

1.2 试除法的优化:从 O (n) 到 O (√n)

我们可以通过一个关键性质优化判定过程:如果 a 是 x 的约数,那么 x/a 也一定是 x 的约数。这意味着我们无需检查到 x-1,只需检查到√x 即可。因为如果 x 存在大于√x 的因数,那么它对应的另一个因数必然小于√x,在此之前就已经被检查过了。

举个例子,判断 100 是否为质数:√100=10,我们只需检查 2 到 10 之间的数。发现 2 能整除 100,直接判定 100 为合数,无需继续检查后续数字。

此外,在代码实现时,为了避免使用 sqrt 函数带来的精度问题,我们可以采用i <= x / i的写法,既保证了逻辑正确性,又提升了代码效率。

1.3 C++ 实现质数判定函数

bool isprime(int x) { if (x <= 1) return false; // 小于等于1的数直接排除 // 试除法核心:枚举到√x即可 for (int i = 2; i <= x / i; ++i) { if (x % i == 0) return false; // 存在其他因数,不是质数 } return true; // 未找到其他因数,是质数 }

这个函数的时间复杂度为O (√x),对于 1e9 以内的单个数字判定完全足够。比如判断 1e9+7 是否为质数,只需循环约 3e4 次,效率极高。

1.4 实战例题:洛谷 P5736 质数筛

题目链接:https://www.luogu.com.cn/problem/P5736

题目描述:输入 n 个不大于 1e5 的正整数,去除掉不是质数的数字,依次输出剩余的质数。

解题思路:对于每个输入的数字,调用上述 isprime 函数进行判定,将判定为质数的数字输出即可。

C++ 参考代码

#include <iostream> using namespace std; bool isprime(int x) { if (x <= 1) return false; for (int i = 2; i <= x / i; ++i) { if (x % i == 0) return false; } return true; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin >> n; for (int i = 0; i < n; ++i) { int x; cin >> x; if (isprime(x)) { cout << x << " "; } } cout << endl; return 0; }

代码优化说明:使用ios::sync_with_stdio(false);cin.tie(nullptr);关闭输入输出同步,提升大数据量下的读取速度,避免因输入输出耗时导致超时。

二、筛法入门:埃氏筛法(Eratosthenes Sieve)

2.1 筛法的核心思想

当需要找出 [1, n] 范围内的所有质数时,逐个判定的方法效率较低(时间复杂度 O (n√n))。筛法的核心思想是 “标记非质数”:利用质数的倍数一定是合数的性质,从 2 开始,将每个质数的所有倍数标记为合数,未被标记的数即为质数。

2.2 埃氏筛法的基本实现

  1. 初始化一个布尔数组 st,st [i]表示数字 i 是否被标记为合数(初始值均为 false)。
  2. 从 2 开始遍历到 n:
    • 如果 st [i] 为 false,说明 i 是质数,将其记录下来。
    • 然后将 i 的所有倍数(从 ii 开始,而非 2i)标记为 true。这里从 ii 开始是因为小于 ii 的倍数已经被更小的质数标记过了(比如 i=5 时,52=10 已被 2 标记,53=15 已被 3 标记,只需从 5*5=25 开始标记)。

2.3 C++ 实现埃氏筛法

const int MAXN = 1e6 + 10; bool st[MAXN]; // 标记是否为合数 int primes[MAXN], cnt; // 存储质数,cnt为质数个数 void eratosthenes_sieve(int n) { memset(st, false, sizeof st); cnt = 0; for (int i = 2; i <= n; ++i) { if (!st[i]) { // i是质数 primes[++cnt] = i; // 从i*i开始标记倍数 for (long long j = 1LL * i * i; j <= n; j += i) { st[j] = true; } } } }

2.4 埃氏筛法的时间复杂度与优化

埃氏筛法的时间复杂度为O (n log log n),这个复杂度非常接近线性,对于 n=1e6 的范围,几乎可以瞬间完成筛选。

关键优化点

  • 使用long long类型的 j 避免溢出:当 i 接近√n 时,ii 可能超过 int 的范围(比如 i=1e5 时,ii=1e10,超过 int 的最大值 2e9),因此需要用 1LL 强制转换为长整型。
  • 数组初始化优化:使用memset函数快速初始化数组,效率大大高于循环赋值。

2.5 实战例题:洛谷 P3383 模板线性筛素数

题目链接:https://www.luogu.com.cn/problem/P3383

题目描述:给定范围 n 和 q 个询问,每次输出第 k 小的素数。

解题思路:先用埃氏筛法筛选出 [1, n] 范围内的所有质数,存储在 primes 数组中,然后直接响应每个询问。

C++ 参考代码

#include <iostream> #include <cstring> using namespace std; const int MAXN = 1e8 + 10; // 根据题目n的范围调整 bool st[MAXN]; int primes[MAXN], cnt; void eratosthenes_sieve(int n) { memset(st, false, sizeof st); cnt = 0; for (int i = 2; i <= n; ++i) { if (!st[i]) { primes[++cnt] = i; for (long long j = 1LL * i * i; j <= n; j += i) { st[j] = true; } } } } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, q; cin >> n >> q; eratosthenes_sieve(n); while (q--) { int k; cin >> k; cout << primes[k] << endl; } return 0; }

注意事项:当 n 的范围较大(如 1e8)时,需要注意数组的内存占用。bool 类型数组每个元素占 1 字节,1e8+10 的数组约占 100MB,在竞赛允许的内存范围内。

三、筛法巅峰:线性筛法(欧拉筛法)

3.1 埃氏筛法的局限性

埃氏筛法虽然高效,但存在一个问题:某些合数会被多个质数重复标记。比如 12,会被 2 标记一次,又会被 3 标记一次。这虽然不影响最终结果,但造成了不必要的计算开销。

线性筛法(又称欧拉筛法)的核心创新点是:让每个合数只被其最小质因数标记一次,从而实现 O (n) 的线性时间复杂度,是竞赛中筛选质数的最优方法。

3.2 线性筛法的核心原理

  1. 同样使用 st 数组标记合数,primes 数组存储已找到的质数。
  2. 遍历每个数 i(从 2 到 n):
    • 如果 i 未被标记,说明 i 是质数,将其加入 primes 数组。
    • 遍历 primes 数组中的每个质数p [j]
      • 计算ip [j],如果ip [j] > n则跳出循环。
      • i*p [j]标记为合数。
      • 关键步骤:如果 i 能被 p [j] 整除,立即跳出循环。因为此时 p [j] 是 i 的最小质因数,也是 ip [j] 的最小质因数;如果继续遍历更大的质数,就会导致 ip [j] 被更大的质因数标记,违背 “只被最小质因数标记” 的原则。

3.3 线性筛法的逻辑拆解

我们以 i=6 为例,详细拆解线性筛法的执行过程:

  • primes 数组中已有 [2, 3, 5]。
  • 遍历 p [j]=2:6*2=12,标记 12 为合数;由于 6%2==0,立即跳出循环。
  • 这里 12 的最小质因数是 2,因此只被 2 标记一次,不会再被 3 标记(6*3=18,18 会在 i=9 时被 3 标记)。

再以 i=5(质数)为例:

  • 遍历 p [j]=2:5*2=10,标记 10(最小质因数 2)。
  • p [j]=3:5*3=15,标记 15(最小质因数 3)。
  • p [j]=5:5*5=25,标记 25(最小质因数 5)。
  • p [j]=7:5*7=35>n(假设 n=30),跳出循环。

通过这种方式,每个合数都只被其最小质因数标记一次,确保了线性时间复杂度。

3.4 C++ 实现线性筛法

const int MAXN = 1e7 + 10; bool st[MAXN]; int primes[MAXN], cnt; void euler_sieve(int n) { memset(st, false, sizeof st); cnt = 0; for (int i = 2; i <= n; ++i) { if (!st[i]) { // i是质数 primes[++cnt] = i; } // 遍历所有已找到的质数,标记i*primes[j]为合数 for (int j = 1; 1LL * i * primes[j] <= n; ++j) { st[i * primes[j]] = true; if (i % primes[j] == 0) { // primes[j]是i的最小质因数 break; } } } }

3.5 线性筛法的优势与应用场景

线性筛法的时间复杂度严格为O (n),在 n=1e7 的范围内,其速度远快于埃氏筛法。更重要的是,线性筛法在筛选质数的同时,还能方便地求出每个数的最小质因数,这为后续的质因数分解、欧拉函数计算等操作提供了极大便利。

四、进阶实战:质数相关经典例题解析

4.1 例题 1:洛谷 P1835 素数密度

题目链接:https://www.luogu.com.cn/problem/P1835

题目描述:给定 L 和 R,计算区间 [L, R] 中素数的个数(1≤L≤R<2^31,R-L≤1e6)。

难点分析:R 的范围最大为 2^31-1,直接筛选 [1, R] 范围内的质数会导致数组过大(需要 2^31 字节,约 2GB),内存无法承受。

解题思路:利用 “区间筛法”,核心思想是:

  1. 对于区间 [L, R] 中的数,其质因数一定不大于√R。因此先筛选出 [1, √R] 范围内的所有质数。
  2. 用这些质数去标记 [L, R] 范围内的倍数,未被标记的数即为质数。

C++ 参考代码

#include <iostream> #include <cstring> #include <cmath> using namespace std; typedef long long LL; const int MAXN = 1e6 + 10; bool st_prime[MAXN]; // 标记[1, sqrt(R)]的质数 bool st_range[MAXN]; // 标记[L, R]的合数 int primes[MAXN], cnt; // 筛选[1, n]的质数 void sieve(LL n) { memset(st_prime, false, sizeof st_prime); cnt = 0; for (LL i = 2; i <= n; ++i) { if (!st_prime[i]) { primes[++cnt] = i; for (LL j = i * i; j <= n; j += i) { st_prime[j] = true; } } } } int count_primes(LL L, LL R) { memset(st_range, false, sizeof st_range); // 用[1, sqrt(R)]的质数标记[L, R]的倍数 for (int i = 1; i <= cnt; ++i) { LL p = primes[i]; // 计算区间[L, R]中p的最小倍数:max(p*2, ceil(L/p)*p) LL start = max(2LL * p, (L + p - 1) / p * p); for (LL j = start; j <= R; j += p) { st_range[j - L] = true; // 映射到数组索引 } } // 统计未被标记的数(质数) int res = 0; for (LL i = L; i <= R; ++i) { if (i < 2) continue; // 小于2的数不是质数 if (!st_range[i - L]) { ++res; } } return res; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); LL L, R; cin >> L >> R; LL sqrt_R = sqrt(R); sieve(sqrt_R); cout << count_primes(L, R) << endl; return 0; }

代码解析

  • 由于 R-L≤1e6,st_range 数组只需开 1e6+10 的大小,内存占用仅 1MB 左右,完全可行。
  • 计算 start 时,使用(L + p - 1) / p * p可以快速求出大于等于 L 的最小 p 的倍数,避免了浮点数运算。

4.2 例题 2:UVA543 Goldbach's Conjecture

题目链接:https://www.luogu.com.cn/problem/UVA543

题目描述:验证哥德巴赫猜想:任意一个大于 4 的偶数都可以拆成两个奇质数之和。对于每个输入的偶数 n,输出 b-a 最大的一组解(a≤b,a 和 b 均为奇质数)。

解题思路

  1. 先用线性筛法筛选出 1e6 以内的所有质数(题目要求验证小于 1e6 的数)。
  2. 对于每个偶数 n,从最小的奇质数 3 开始遍历,检查 n-a 是否为奇质数。找到第一组满足条件的 (a, n-a) 即为 b-a 最大的解(因为 a 越小,b 越大,差值越大)。

C++ 参考代码

#include <iostream> #include <cstring> using namespace std; const int MAXN = 1e6 + 10; bool st[MAXN]; int primes[MAXN], cnt; void euler_sieve(int n) { memset(st, false, sizeof st); cnt = 0; for (int i = 2; i <= n; ++i) { if (!st[i]) { primes[++cnt] = i; } for (int j = 1; 1LL * i * primes[j] <= n; ++j) { st[i * primes[j]] = true; if (i % primes[j] == 0) { break; } } } } void solve(int n) { // 从最小的奇质数开始查找 for (int i = 2; i <= cnt; ++i) { int a = primes[i]; int b = n - a; if (b < a) continue; // 保证a<=b if (!st[b]) { // b是质数 printf("%d = %d + %d\n", n, a, b); return; } } printf("Goldbach's conjecture is wrong.\n"); } int main() { euler_sieve(1e6); // 预处理1e6以内的质数 int n; while (cin >> n && n) { solve(n); } return 0; }

优化说明:预处理 1e6 以内的质数后,每个查询的时间复杂度为 O (π(√n))(π(x) 表示 x 以内的质数个数),效率极高,即使处理多组数据也能快速响应。

五、质数相关算法的常见误区与优化技巧

5.1 常见误区

  1. 整数溢出问题:在计算i*p [j]p 的幂次时,容易超出 int 的范围,导致程序出错。解决方案是使用 long long 类型进行中间计算。
  2. 数组初始化错误:筛法中数组未初始化或初始化不完全,导致标记错误。建议使用 memset 函数进行初始化,注意 memset 按字节赋值,仅适用于 bool、char 数组或清零 int 数组。
  3. 边界条件处理不当:比如将 1 判定为质数、筛选时从 1 开始遍历、未处理 L=1 的情况等。需要严格遵循质数的定义,仔细处理边界。

5.2 优化技巧

  1. 内存优化:对于大范围筛选(如 1e8),可以使用 bitset 代替 bool 数组,将内存占用减少到原来的 1/8(bitset 中每个元素占 1 位)。
  2. 输入输出优化:竞赛中大数据量的输入输出会占用大量时间,建议使用ios::sync_with_stdio(false);cin.tie(nullptr);或用scanf/printf代替cin/cout
  3. 预处理优化:对于多组查询的题目,提前预处理出最大范围的质数和相关信息(如质因子、欧拉函数等),避免重复计算。

总结

质数判定与筛法是算法竞赛数论部分的基础,掌握这些技能不仅能解决直接的质数相关问题,还能为后续学习质因数分解、欧拉函数、同余方程等复杂数论内容打下坚实基础。

在实际竞赛中,质数相关问题往往会与其他数论知识结合考查,比如与欧拉函数结合的 GCD 计数问题、与中国剩余定理结合的模运算问题等。因此,除了掌握本文介绍的内容,还需要不断拓展知识面,多做综合性题目,提升知识的融会贯通能力。

最后,建议大家多动手实现代码,通过调试理解算法的核心逻辑,同时关注题目中的数据范围,选择合适的算法和优化方式。只有通过大量练习,才能在竞赛中快速准确地解决质数相关问题,拿到关键分数。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/17 9:01:57

Vivado使用实战:手把手实现FPGA流水灯项目

手把手带你用Vivado点亮FPGA上的第一盏“流水灯” 你有没有过这样的经历&#xff1f;买了一块Xilinx FPGA开发板&#xff0c;兴冲冲地插上电&#xff0c;打开Vivado却一脸懵&#xff1a;工程怎么建&#xff1f;代码写在哪&#xff1f;引脚怎么连&#xff1f;为什么下载后LED不亮…

作者头像 李华
网站建设 2026/4/18 7:57:32

Prompt工程治理:如何建立语义级Diff评审与行为回归测试流程?

在智能体系统逐渐走向复杂化之后,许多团队都会意识到一个问题:系统行为发生变化,却很难追溯原因。模型版本没有变,核心代码没有改,工具接口依然正常,但输出结果却悄然偏移。最终排查下来,往往是某一段 Prompt 被“顺手优化”过。 这类问题的出现,标志着一个事实:Prom…

作者头像 李华
网站建设 2026/4/14 9:18:20

高效科研必备:Miniconda+PyTorch环境复现实践

高效科研必备&#xff1a;MinicondaPyTorch环境复现实践 在深度学习项目中&#xff0c;最令人头疼的往往不是模型设计或训练调参&#xff0c;而是“在我机器上明明能跑”的尴尬局面。你辛辛苦苦训练出一个高精度模型&#xff0c;分享代码给合作者时&#xff0c;对方却因为缺少某…

作者头像 李华
网站建设 2026/4/18 8:31:40

物理信息神经网络完全指南:从零基础到实战应用的5大突破

物理信息神经网络&#xff08;PINN&#xff09;正在重塑科学计算的未来&#xff0c;这种融合深度学习与物理定律的创新方法让复杂微分方程求解变得前所未有的简单高效。作为科学计算领域的新手&#xff0c;你可能还在为传统的数值方法头疼不已&#xff0c;但现在有了PINNpapers…

作者头像 李华
网站建设 2026/4/18 10:04:40

现代电力系统规划完整解析:从理论到实践的终极指南

现代电力系统规划完整解析&#xff1a;从理论到实践的终极指南 【免费下载链接】电力系统设计手册10273.pdf简介 《电力系统设计手册10273.pdf》是电力系统规划设计领域的权威指南&#xff0c;为技术人员和研究人员提供全面且实用的参考。手册深入解析电力负荷预测、电力电量平…

作者头像 李华
网站建设 2026/4/18 3:03:05

PyTorch Fairseq神经机器翻译:从入门到精通的完整实践指南

PyTorch Fairseq神经机器翻译&#xff1a;从入门到精通的完整实践指南 【免费下载链接】fairseq 项目地址: https://gitcode.com/gh_mirrors/fai/fairseq 你是否曾经为机器翻译的复杂性而头疼&#xff1f;想要快速上手一个强大的翻译工具&#xff0c;却不知道从何开始&…

作者头像 李华