当前位置: 首页 > news >正文

信竞中的数学(一):质数

基本概念梳理

质数:对于所有大于 1 1 1 的自然数而言,如果该数除 1 1 1 和自身以外没有其它因数/约数,则该数被称为为质数,质数也叫素数。

合数:对于大于1的整数中,除了1和该数本身以外,还有其他因数,最小的合数是4,1既不是质数也不是合数。

约数:如果一个数 x x x 可以整除另外一个数 y y y,则 x x x 称为 y y y 的约数,约数也叫因数。

[ x , y ] [x, y] [x,y] : 闭区间,即包含两个端点 x x x y y y,比如区间 [ 3 , 6 ] [3, 6] [3,6] 等价于 3 , 4 , 5 , 6 {3, 4, 5, 6} 3,4,5,6 四个数。

⌊ x ⌋ \lfloor x \rfloor x 表示 向下取整,即 ⌊ 5.7 ⌋ = 5 \lfloor 5.7 \rfloor = 5 5.7=5 ⌊ 3 ⌋ = 3 \lfloor 3 \rfloor = 3 3=3

质数的应用

【例题1】试除法判定质数

给定 n ( 1 ≤ n ≤ 100 ) n(1\leq n \leq 100) n(1n100) 个正整数 a i ( 1 ≤ a i ≤ 2 31 − 1 ) a_i(1\leq a_i \leq 2^{31}-1) ai(1ai2311),判定每个数是否是质数。

输入格式

第一行包含整数 n n n

接下来 n n n 行,每行包含一个正整数 a i a_i ai

输出格式

n n n 行,其中第 i i i 行输出第 i i i 个正整数 a i a_i ai 是否为质数,是则输出 Yes,否则输出 No

输入样例:
2
2
6
输出样例:
Yes
No
【解析】试除法判定质数

如何判定一个数是否为质数呢?

最简单的办法就是试除法,即判断 x x x 是否为质数,可以枚举闭区间 [ 2 , x − 1 ] [2, x-1] [2,x1] 中的所有整数,去尝试整除 x x x

如果区间中存在可以整除 x x x 的整数,则 x x x 不是质数;

如果闭区间中所有数都不能整除 x x x,则 x x x 为质数, 代码如下:

// isPrime: 判定 x 是否为质数,如果是,返回 true, 否则返回 false
bool isPrime(int x) {if(x < 2) return false;  // 质数的定义中只考虑大于 1 的数for(int i = 2; i < x; i++) {if(x % i == 0) return false;  // 在 [2, x-1] 中出现了约数,则可以断定不是质数}return true;  // 遍历完所有的数都没有找到约数,则一定为质数
}

以上代码的时间复杂度为 O ( x ) O(x) O(x),是否可以优化呢?答案是可以的。

我们可以优化一下试除的区间,可以将区间缩小为 [ 2 , ⌊ x ⌋ ] [2, \lfloor \sqrt{x}\rfloor \ ] [2,x  ] ,思考为什么可以缩小到这个区间呢?

因为约数是成对出现的,比如 12 12 12, 约数有 2 , 3 , 4 , 6 2,3,4,6 2346,其中 ( 2 , 6 ) (2, 6) (2,6) 是一对, ( 3 , 4 ) (3, 4) (3,4) 是一对;

如果该数 x x x 有约数,我们可以只枚举较小的约数,整除结果就是对应的较大的约数;

因此可以缩小枚举区间到 [ 2 , ⌊ x ⌋ ] [2, \lfloor \sqrt{x}\rfloor \ ] [2,x  ],如果在区间 [ 2 , ⌊ x ⌋ ] [2, \lfloor \sqrt{x}\rfloor \ ] [2,x  ] 中不存在 x x x 的约数,那在 [ ⌊ x ⌋ + 1 , x − 1 ] [\lfloor \sqrt{x} \rfloor + 1, x-1] [⌊x +1,x1] 区间中也不会存在 x x x 的约数。

但是在计算机中,求 x \sqrt{x} x 的效率更慢一些,所以我们可以等价替换为 i ∗ i ≤ x i*i \le x iix,但是在极端情况下,可能会出现 i ∗ i i*i ii 越界导致出现错误,因此最推荐的写法为 i ≤ x / i i \le x/i ix/i, 代码如下:

// isPrime: 判定 x 是否为质数,如果是,返回 true, 否则返回 false
bool isPrime(int x) {if(x < 2) return false;  // 质数的定义中只考虑大于 1 的数for(int i = 2; i <= x/i; i++) {if(x % i == 0) return false;  // 在 [2, x-1] 中出现了约数,则可以断定不是质数}return true;  // 遍历完所有的数都没有找到约数,则一定为质数
}

此时,时间复杂度优化为 O ( x ) O(\sqrt{x}) O(x ),已达到最优。

【本题题解完整代码】
#include "bits/stdc++.h"
using namespace std;
int x, n;
// isPrime: 判定 x 是否为质数,如果是,返回 true, 否则返回 false
bool isPrime(int x) {if(x < 2) return false;  // 质数的定义中只考虑大于 1 的数for(int i = 2; i <= x/i; i++) {if(x % i == 0) return false;  // 在 [2, x-1] 中出现了约数,则可以断定不是质数}return true;  // 遍历完所有的数都没有找到约数,则一定为质数
}
int main() {cin >> n;while(n--) {cin >> x;if(isPrime(x)) cout << "Yes\n";else cout << "No\n";}return 0;
}

【例题2】分解质因数

给定 n ( 1 ≤ n ≤ 100 ) n(1\leq n \leq 100) n(1n100) 个正整数 a i ( 2 ≤ a i ≤ 2 × 1 0 9 ) a_i(2\leq a_i \leq 2 \times 10^{9}) ai(2ai2×109),将每个数分解质因数,并按照质因数从小到大的顺序输出每个质因数的底数和指数。

输入格式

第一行包含整数 n n n

接下来 n n n 行,每行包含一个正整数 a i a_i ai

输出格式

对于每个正整数 a i a_i ai,按照从小到大的顺序输出其分解质因数后,每个质因数的底数和指数,每个底数和指数占一行。

每个正整数的质因数全部输出完毕后,输出一个空行。

输入样例:
2
6
8
输出样例:
2 1
3 12 3
【解析】分解质因数

任何一个大于 1 1 1 的数字都可以分解为若干个质数相乘,这个过程也叫分解质因数。

例如: 12 = 2 × 2 × 3 , 16 = 2 × 2 × 2 × 2 , . . . . . . 12 = 2 \times 2 \times 3, 16 = 2 \times 2 \times 2 \times 2, ...... 12=2×2×3,16=2×2×2×2,......

给定一个数 x x x,如果将该数分解成若干质因数相乘呢?通过试除法即可完成。

遍历闭区间 [ 2 , x ] [2, x] [2x],如果当前遍历到的数字 i i i 可以整除 x x x,那么就用 i i i 整除 x, 直到不能整除即可停止,统计整除的次数 c n t cnt cnt 即为质因数 i i i 的指数。

代码如下:

// 将 x 分解质因数
void deal(int x) {for(int i=2; i<=x; i++) {  // 试除区间int cnt = 0;   // 当前质因数的个数while(x % i == 0) {   // 如果 i 可以整除,那么一直整除统计个数cnt++;x /= i;   // x 被整除}if(cnt > 0) cout << i << ' ' << cnt << endl;   // 当前 i 是质因数}cout << endl;
}

上面的代码,有同学会有疑惑,不是说分解质因数嘛?为什么没有判断 i i i 为质数呢?

如果 x % i = = 0 x \% i == 0 x%i==0 成立的话, i i i 一定就是质数;

因为此时 x x x [ 2 , i − 1 ] [2, i-1] [2,i1] 中已经没有因数了(都被整除掉了),所以如果 x % i = = 0 x \% i == 0 x%i==0 成立的话,说明 i i i [ 2 , i − 1 ] [2, i-1] [2,i1] 区间中也没有因数了,即 i i i 为质数。

上面的代码时间复杂度为 O ( x ) O(x) O(x),能否优化呢?可以的。

我们知道, x x x 最多有一个大于 x \sqrt{x} x 的质因数,因此可以将遍历区间缩小到 [ 2 , x ] [2, \sqrt{x}] [2,x ] 中,遍历结束后,如果 x x x 依然大于 1 1 1,那此时 x x x 一定就是那个质因数。

代码如下:

// 将 x 分解质因数
void deal(int x) {for(int i=2; i<=x/i; i++) {  // 试除区间int cnt = 0;  // 当前质因数的个数while(x % i == 0) {  // 如果 i 可以整除,那么一直整除统计个数cnt++;x /= i;  // x 被整除}if(cnt > 0) cout << i << ' ' << cnt << endl;  // 当前 i 是质因数}if(x > 1) cout << x << ' ' << 1 << endl;  // 最后一个最大的质因数cout << endl;
}

由于分解质因数过程中, x x x 一直被整除,因此假设最好的情况, x x x 刚好是 2 2 2 的次幂,那此时,代码时间复杂度为 O ( l o g x ) O(log\ x) O(log x)

因此总的时间复杂度为 O ( l o g x ) O(log\ x) O(log x) O ( x ) O(\sqrt{x}) O(x ) 之间。

【本题题解完整代码】
#include "bits/stdc++.h"
using namespace std;
const int N = 1e6+7;
int n, x;
// 将 x 分解质因数
void deal(int x) {for(int i=2; i<=x/i; i++) {  // 试除区间int cnt = 0;  // 当前质因数的个数while(x % i == 0) {  // 如果 i 可以整除,那么一直整除统计个数cnt++;x /= i;  // x 被整除}if(cnt > 0) cout << i << ' ' << cnt << endl;  // 当前 i 是质因数}if(x > 1) cout << x << ' ' << 1 << endl;  // 最后一个最大的质因数cout << endl;
}
int main() {cin >> n;while(n--) {cin >> x;deal(x);}return 0;
}

【例题3】线性筛质数

题目描述

如题,给定一个范围 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 行,每行一个正整数表示答案。

输入
100 5
1
2
3
4
5
输出
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 1q106,保证查询的素数不大于 n n n

【解析】线性筛质数

根据上面分解质因数可以知道,任何一个合数均可以分解为若干个质因数相乘,那是否可以将所有的合数标记出来, 2 ~ n 2~n 2n 中,所有已标记的合数的补集就是所有质数。

为什么要这样做呢?因为如果直接找质数,一定需要对每个数进行 c h e c k check check 判断,但是对于合数来说,不需要判断,我们可以通过乘积的方式进行快速标记。

如何记录第 k k k 个素数是谁呢?在标记的时候,由于标记是往后进行的,所以遍历到 i i i 的时候,如果 i i i 没有被标记,那么一定就是质数, c n t cnt cnt 1 1 1 开始,遇到一个质数,保存起来,并 c n t + + cnt++ cnt++,表示寻找下一个质数。

【算法步骤】

1 1 1、 定义 i s N o t P r i m e [ i ] isNotPrime[i] isNotPrime[i] t r u e true true 表示 i i i 不是质数,即 i i i 是合数;

定义 p r i m e [ i ] prime[i] prime[i] 表示第 i i i 个质数;比如 p r i m e [ 1 ] = 2 , p r i m e [ 2 ] = 3 , p r i m e [ 3 ] = 5 , . . . prime[1]=2,prime[2]=3, prime[3]=5,... prime[1]=2prime[2]=3,prime[3]=5,... 表示第一个质数是 2 2 2,第二个质数是 3 3 3,第三个质数是 5 , . . . 5,... 5... 等等

2 2 2、遍历 i i i i i i 属于 [ 2 , n ] [2,n] [2,n] 闭区间, 将 i i i 之前的所有质数都保存在 p r i m e prime prime 数组中,可以用 i i i 乘以所有保存的质数得到 y y y,那么 y y y 一定不是质数,所以可以标记 i s N o t P r i m e [ y ] isNotPrime[y] isNotPrime[y] t r u e true true,遍历到 i i i 以后,如果 i s N o t P r i m e [ i ] isNotPrime[i] isNotPrime[i] 依然为 f a l s e false false,没有被标记,那么可以说 i i i 就一定是质数,需要保存起来。

3 3 3、对于有些合数比如 12 12 12,既可以分解为 2 × 6 2\times 6 2×6,也可以分解为 3 × 4 3\times 4 3×4,如果不加入限制,则很多数会被大量重复标记,导致效率低下,时间复杂度上升。

如何限制呢?对于 i i i 而言,如果 i % p r i m e [ j ] = = 0 i\%prime[j]==0 i%prime[j]==0,则说明一定存在一个数 x x x,使得 p r i m e [ j ] × x = = i prime[j]\times x==i prime[j]×x==i(公式 1 1 1);

如果此时继续往后走,将会标记 p r i m e [ j + 1 ] × i prime[j+1]\times i prime[j+1]×i 为非质数,用公式 1 1 1 替换,可以得到下一个要标记 p r i m e [ j + 1 ] × p r i m e [ j ] × x prime[j+1]\times prime[j]\times x prime[j+1]×prime[j]×x

由于 p r i m e [ j ] < p r i m e [ j + 1 ] prime[j] < prime[j+1] prime[j]<prime[j+1] ( $ prime[j]$ 是第 j j j 个质数, p r i m e [ j + 1 ] prime[j+1] prime[j+1] 是第 j + 1 j+1 j+1 个质数),

因此下一个要标记的数 p r i m e [ j + 1 ] × p r i m e [ j ] × x prime[j+1]\times prime[j]\times x prime[j+1]×prime[j]×x 可以在 i = = p r i m e [ j + 1 ] × x i==prime[j+1]\times x i==prime[j+1]×x 的时候用更小的质数 p r i m e [ j ] prime[j] prime[j] 进行标记,

因此就不需要大的质数 p r i m e [ j + 1 ] prime[j+1] prime[j+1] 乘以小的 i = = p r i m e [ j ] × x i==prime[j] \times x i==prime[j]×x 来标记了;

举例说明:比如当 i i i 循环到 4 4 4 的时候,此时保存的质数有 2 2 2 3 3 3 ,那么 2 × 4 = 8 2\times 4=8 2×4=8,所以标记 8 8 8,由于 4 % 2 = = 0 4\%2==0 4%2==0 ,所以就可以停止了,不用再往后标记了,再往后就是 3 × 4 3\times 4 3×4 标记 12 12 12 了,由于 i i i 循环到 6 6 6 的时候,可以用 2 × 6 2\times 6 2×6 标记 12 12 12,因为质数 2 2 2 小于 质数 3 3 3,所以对于同一个数,我们用最小质因数标记,可以防止重复标记,增加效率。

【本题题解完整代码】
#include "iostream"
using namespace std;
int n , q , k , x;
const int N = 1e8+7;
bool isNotPrime[N];   // 如果isNotPrime[i]为true,则i不是素数,否则i是素数
int prime[N];    // prime[i]表示第i个素数是多少
int cnt = 1;  // cnt表示当前将要存储第cnt个质数,cnt从1开始
int main()
{cin >> n >> q;for (int i = 2;i <= n;i++) {       // i从2遍历到nif(!isNotPrime[i]) prime[cnt++] = i;   // 如果i没有被标记过,说明i一定是质数,保存到prime数组中,cnt++,要保存下一个质数了for(int j=1; j<cnt && prime[j] * i <= n; j++) {  // 找出目前已经保存的所有质数,与i做乘积,乘积后标记为非质数isNotPrime[i*prime[j]] = true;   // 标记非质数if(i%prime[j] == 0) break;    // 遇到这种情况就break,因为后面的非质数,可以用更小的质数标记,只需要i大一点就行了,}}while(q--) {cin >> x;  // 询问cout << prime[x] << endl;  // 直接输出即可}return 0;
}

相关文章:

  • 典籍查询界面增加我的收藏查询功能
  • 极狐GitLab 议题权重有什么作用?
  • 【漫话机器学习系列】227.信息检索与数据挖掘中的常用加权技术(TF-IDF)
  • 论文检索相关网站
  • 制作一款打飞机游戏26:精灵编辑器
  • 【2025 最新前沿 MCP 教程 05】为 MCP 设置开发环境
  • 《AI大模型应知应会100篇》第37篇:Agent框架入门:让AI具备自主行动能力
  • 非结构化数据解析
  • ESP32开发入门(四):ESP32-s3多串口开发实践
  • Linux进程详细解析
  • Day14(链表)——LeetCode234.回文链表141.环形链表
  • MySQL:13.用户管理
  • 【漫话机器学习系列】226.测试集、训练集、验证集(test,training,validation sets)
  • 天线设计实战:三大经典布局的摆放逻辑与核心技术要点!
  • el-input限制输入只能是数字 限制input只能输入数字
  • 力扣hot100,739每日温度(单调栈)详解
  • 什么是模块化区块链?Polkadot 架构解析
  • 【今日三题】笨小猴(模拟) / 主持人调度(排序) / 分割等和子集(01背包)
  • Pinia——Vue的Store状态管理库
  • 【KWDB创作者计划】_企业级多模数据库实战:用KWDB实现时序+关系数据毫秒级融合(附代码、性能优化与架构图)
  • 全球前瞻|王毅赴巴西出席金砖外长会,加拿大迎来“几十年来最重要大选”
  • 国家发改委:建立实施育儿补贴制度
  • 中纪报:五一节前公开通报释放强烈信号,以铁律狠刹歪风邪气
  • 印方称与巴基斯坦军队在克什米尔交火
  • 地下管道密布成难题,道路修整如何破局?
  • 伊朗外长: 美伊谈判进展良好,讨论了很多技术细节