数学基础算法——质数篇

文章目录

质数

定义

质数(英文名:Primenumber)又称素数,是指在大于1的自然数中,除了1和它本身以外不再有其他因数的自然数。而大于1又不是指数的正整数称为合数(规定1既不是质数也不是合数)。


质数的Basic Information

发展历史

质数的研究最早可追溯到古埃及。古希腊的毕达哥拉斯学派,欧几里得,和埃拉托斯特尼等人对质数有不少研究。在公元前 1550 年左右的古埃及的莱因德数学纸草书中就有对素数与对合数完全不同类型的记录。古希腊的毕达哥拉斯学派(英语:Pythagoras)对质数有不少研究。他们把"2"排除在质数之外,因为"2"不是"真正的数" 。公元前300年左右的欧几里得(希腊语:Ευκλειδης)所著的《几何原本》包含与素数有关的重要定理,如有无限多个素数,以及算术基本定理。埃拉托斯特尼(古希腊语:Eratosthenes Sieve)提出的埃拉托斯特尼筛法是用来计算素数的一个简单方法。近现代数学中,皮埃尔·德·费马,法国博学家马林·梅森,德国数学家克里斯蒂安·哥德巴赫和瑞士数学家欧拉等人得到了一些关于质数的重要成果。1640年,皮埃尔·德·费马(法语:Pierre de Fermat)叙述了费马小定理,费马还研究了费马数的素数。法国博学家马林·梅森(法语:Marin Mersenne)发现了一类素数,即梅森素数。 德国数学家克里斯蒂安·哥德巴赫(德语:Goldbach C)在 1742 年写给欧拉的一封信中提出了哥德巴赫的猜想,即每个偶数都是两个素数之和 。瑞士数学家欧拉(德语:Leonhard Euler)有许多和质数有关的成果。他证明了素数倒数和的无穷级数会发散 。法国数学家阿德里安-马里·勒让德与德国数学家约翰·卡尔·弗里德里希·高斯各自独立证明了素数定理。法国数学家雅克·所罗门·阿达马和比利时数学家夏尔-让·德拉瓦莱·普桑完成了素数定理的初等证明。不过质数依然有许多悬而未决的理论,比如著名的哥德巴赫猜想等。

------摘自百度百科

作用

  1. 密码学:质数在密码学中起着关键作用,特别是在公钥加密算法如RSA中。这些算法的安全性基于大质数的分解难度。所谓的公钥就是将想要传递的信息在编码时加入质数,编码之后传送给收信人,任何人收到此信息后,若没有此收信人所拥有的密钥,则解密的过程中(实为寻找素数的过程),将会因为找质数的过程(分解质因数)过久,使即使取得信息也会无意义。
  2. 计算机科学:在计算机科学中,质数被用于生成随机数,特别是在安全随机数生成器中。此外,质数还被用于设计和分析算法,如素数筛法。我们今天就要讲这个

质数的相关定理

1.算数基本定理

任何一个大于1的正整数都能唯一分解为有限个质数的乘积

2.质数分布定理

对于正实数 x x x ,定义 π ( x ) \pi(x) π(x) 为不大于 x x x 的质数的个数,则有: π ( x ) ≈ x I n x \pi (x) \approx \frac{x}{Inx} π(x)≈Inxx

由此我们可以给出第 n n n 个质数 p ( n ) p(n) p(n) 的渐进估计: p ( n ) ≈ n l n n p(n) \approx nlnn p(n)≈nlnn


质数的判定

易错提醒:若整数b除以非零整数a,商为整数,且余数为零,b为被除数,a为除数,即a|b,读作"a整除b"或"b能被a整除"。

试除法

内容:若一个数 N N N 为合数,则一定存在一个能够整除这个数的 k k k ,其中 2 ≤ k ≤ N 2 \le k \le \sqrt N 2≤k≤N
证明:对于任意的 N N N 若存在 u u u 使得 u u u 能整除 N N N ,那么一定存在 N u \frac{N}{u} uN,使 N u \frac{N}{u} uN可以除尽 N N N,故有方程 N / u = u N/u = u N/u=u;所以 u = N u = \sqrt N u=N ;

因此我们只需要扫描 2 → N 2 \to \sqrt N 2→N 之间所有的整数,并且一次检查他们能否整除 N N N ,若都不能,则为质数,若有一个可以,则为合数

cpp 复制代码
bool isprime(){
	for (int i=2;i*i<=sqrt(n);i++){
		if (n%i==0)return false;
	}
	return true;
}

此代码时间复杂度为 O ( N ) O(\sqrt N) O(N )

六倍原理试除法

内容:大于等于5的质数一定和6的倍数相邻,但与6的倍数相邻的不一定是素数,但有可能是6倍邻数的倍数。
命题1:大于等于5的素数必定为6倍邻数证明

证明:

6倍以外的数分别有:6n+1,6n+2,6n+3,6n+4,6n+5,其中6n+2,6n+3,6n+4三个数都可以分解:

6n+2=2(3n+1)

6n+3=3(2n+1)

6n+4=2(3n+2)

所以以上三个数必不可能是素数,剩下的只有6n+1,6n+5可能存在素数,命题得证。
命题2:6n+1和6n+5,只可能为6m+1和6m+5的倍数。

证明:

首先,6m+2,6m+4为偶数,不可能是6n+1,6n+5这个两个奇数的因数,自然就不用判断。

然后,6m+3=3(2m+1),所以6m+3必为3的倍数,但是因为 6 n + 1 3 = 2 n + 1 3 \frac{6n+1}{3}=2n+\frac{1}{3} 36n+1=2n+31 , 6 n + 5 3 = 2 n + 5 3 \frac{6n+5}{3}=2n+\frac{5}{3} 36n+5=2n+35 ,无论 n n n 取何值,都不为整数,所以3的倍数也不分布在6n+1上,也不分布在6n+5上。所以6m+3也不可能是 6n+1和6n+5的因数。

而只有 6m+1 和 6m+5 不能直接判断。所以6n+1和6n+5,只可能为6m+1和6m+5的倍数。

因此我们再扫描的时候,只需要扫描从 2 → N 2 \to \sqrt N 2→N 中为 6n+1 或 6n+5 的数就行了,这样相比上面的方法快了6倍多。

cpp 复制代码
bool isprime(int num) {
	if (num==2||num==3) { //预处理比5要小的质数
		return true;
	}
	//如果不与6的倍数相邻,肯定不是素数
	if (num%6!=1&&num%6!=5) {
		return false;
	}
	//对6倍邻数进行判断,是否为6倍邻数的倍数
	for (int i=5;i*i<=num;i+=6) {
		if (num%i==0||num%(i+2)==0) {
			return false;
		}
	}
	return true;
}

除了这两种方法以外,还有一个miller-rabin质数测试 ,有兴趣的话大家可以去查一下,这个的时间复杂度是 O ( l o g n ) O(logn) O(logn) ,超级快,如果有时间的话,我会在后面出一篇文章专门来将这个的。


质数的筛选

给定一个整数 n n n ,求出 1 → n 1 \to n 1→n 之间所有的质数,称为质数的筛选问题。

暴力筛查

我们可以先单独处理 2 ,然后直接枚举 3 → n 3 \to n 3→n 之间所有的奇数,然后对于每个奇数都进行一次判断,看这个奇数是不是质数,这样的时间复杂度为 O ( n n ) O(n \sqrt n) O(nn ) 。

cpp 复制代码
bool isprime(int num){
	if (num==2||num==3){
		return true;
	}
	if (num%6!=1&&num%6!=5){
		return false;
	}
	for (int i=5;i*i<=num;i+=6){
		if (num%i==0||num%(i+2)){
			return false;
		}
	}
	return true;
}

void primes(int n){
	if (n>=2)cout<<2<<" ";
	for (int i=3;i<=n;i+=2){
		if (isprime(i))cout<<i<<" ";
	}
	return;
}

埃拉托斯特尼筛法

埃拉托斯特尼筛法,简称埃氏筛或爱氏筛,是一种由希腊数学家埃拉托斯特尼所提出的一种简单检定素数的算法。其基本思想是:质数的倍数一定不是质数。

我们可以开一个长度为 n + 1 n+1 n+1 的数组 u s e d used used 来保存信息,看每一个数是否被标记了(0为质数,1为合数)。我们先假设所有的数都是质数,也就是初始化为0,在从小到大枚举每一个 x x x ,如果 u s e d [ x ] = 0 used[x]=0 used[x]=0 ,那么 x x x 就是质数,当然后把质数 x x x 的倍数都标记为非质数,也就是标记为1。

此时,我们发现,一个数可能被多个数都标记了一边,这样的话就会有许多无效的枚举,例如 2 和 3 都会把 6 给标记了。基于此,我们可以发现,小于 x 2 x^2 x2 的 x x x 的倍数在枚举比 x x x 小的数的时候已经被标记了,所以我们完全可以只把大于等于 x 2 x^2 x2 的 x x x 的倍数标记为合数即可。

这样的时间复杂度为 O ( n l o g n l o g n ) O(nlognlogn) O(nlognlogn)

cpp 复制代码
int used[100000];
void primes(int n){
	memset(used,0,sizeof(used));
	for (int i=2;i<=n;i++){
		if (used[i]==1)continue;
		cout<<i<<" ";
		for (int j=i;j*i<=n;j++){
			used[i*j]=1;
		}
	}
}

线性筛法(欧拉筛法)

尽管我们已经对埃拉托斯特尼筛法尽量的优化了,但是还是会有重复的存在,其根本原因是算法不能保证产生一个数的方式,例如 12 = 4 × 3 = 2 × 6 12=4 \times 3= 2 \times 6 12=4×3=2×6。

基于此,我们在生成一个需要被标记的合数的时候,每次只向当前枚举的数 i i i 乘上一个已有的质因子,并且让它是这个合数的最小质因子。相当于让一个合数的质因子从大到小进行积累。思路我们已经有了,现在就是如何实现的问题了。

假设要筛出n以内的素数。我们先把所有数标记为素数。枚举 i i i 从 2 2 2 到 n n n ,所以因为 i i i 是从小到大的,如果 i i i 没有被前面的数(比它小的数)标记为合数,那 i i i 就是素数,加入素数列表。现在用 i i i 来筛后面的数,枚举已经筛出来的素数 p r i m e [ j ] ( 1 ≤ j ≤ m ) prime[j](1 \le j \le m) prime[j](1≤j≤m),标记 i × p r i m e [ j ] i \times prime[j] i×prime[j] 为合数,当 i × p r i m e [ j ] i \times prime[j] i×prime[j] 的最小质因子不为 p r i m e [ j ] prime[j] prime[j] 的时候,退出循环。

但是我们要怎么判断 i × p r i m e [ j ] i \times prime[j] i×prime[j] 的最小质因子到底是不是 p r i m e [ j ] prime[j] prime[j] 呢?

令 k = i × p r i m e [ j ] k= i \times prime[j] k=i×prime[j] ,若 k k k 的最小质因子不为 p r i m e [ j ] prime[j] prime[j] ,则 k k k 的最小质因子一定在 i i i 中并且比 p r i m e [ j ] prime[j] prime[j] 小。也就是说,在 " k k k 的最小质因子不为 p r i m e [ j ] prime[j] prime[j] " 的这个条件下的 i i i 一定为一个以前已经枚举过的一个 p r i m e [ j ] prime[j] prime[j] 的倍数(可以是1倍),那么我们就可以判断当 i i i 为 p r i m e [ j ] prime[j] prime[j] 的倍数的时候,直接跳出循环。 因为如果继续枚举下去的话, i × p r i m e [ j ] i \times prime[j] i×prime[j] 的最小质因子一定不为 p r i m e [ j ] prime[j] prime[j] 了。

这样的时间复杂度就是 O ( n ) O(n) O(n) 的了。

cpp 复制代码
int v[100000],prime[100000];
void primes(int n){
	memset(v,0,sizeof(v));//一个数的最小质因子
	int m=0;//质数的个数 
	v[1]=1;
	for (int i=2;i<=n;i++){
		if (v[i]==0){
			v[i]=1;
			m++;
			prime[m]=i; 
		}
		for (int j=1;j<=m&&i*prime[j]<=n;j++){
			v[i*prime[j]]=1;
			if (i%prime[j]==0)break;
		}
	}
	for (int i=1;i<=m;i++){
		cout<<prime[i]<<" ";
	}
}

区间筛

我们在有些时候只会让我们求解一段的质数的个数,如果两端的范围非常大的话,直接暴力是解决不了的,所以我们要往质数筛的方向去考虑。

我们设我们要求的区间的左右端分别为 l l l , r r r ,其中 l < r l<r l<r 。

我们知道,对于任意合数 x x x ,其最小质因子 p p p 一定 ≤ x \le \sqrt x ≤x 。同理也有对于任意合数 r r r,其最小质因子 ≤ r \le \sqrt r ≤r ,所以区间 l → r l \to r l→r 中的合数只可能被 ≤ r \le \sqrt r ≤r 的素数筛掉。所以如果我们知道了范围 ≤ r \le \sqrt r ≤r 的素数有哪些,我们就可以直接对应到范围在 l → r l \to r l→r 中的数。

也就是说,先分别做好 2 → r 2 \to \sqrt r 2→r 中的表和 l → r l \to r l→r 中的表(也就是初始化全部为素数),然后从第一个表里筛得素数得同时,也将其倍数从第二个表中划去。最后 l → r l \to r l→r 中未被标记的数就是素数。

这样做的好处是可以利用已知的较小范围内的素数表,更高效地筛选出特定区间内的素数,避免了对整个大区间从一开始就进行繁琐的筛选。

现在我们还有一个问题,如果 l l l 和 r r r 非常大的话,数组会存不下,这样应该怎么呢?

这个其实很好想,我们可以把整体的数全部下标偏移,比如说 1 0 9 → 1 0 9 + 5 10^9 \to 10^9+5 109→109+5 的数对应到 1 → 5 1 \to 5 1→5 ,全部减去了一个 1 0 9 10^9 109 。这样就减少了空间的开销

cpp 复制代码
#define ll long long
using namespace std;
const int INF=1e6+10;
long long cnt;
int used[INF],used_s[INF];
long long primes[INF];

void prime(ll a,ll b){//采用的是埃式筛
	for (ll i=2;i*i<=b;i++){
		if (used_s[i]==1)continue;
		for (ll j=2*i;j*j<b;j+=i){
			used_s[j]=1;
		}
		//(a+i-1)/i 为向上取整,得到最接近a的i的倍数,最低是i的2倍,然后筛选
		for (ll j=max(2ll,(a+i-1)/i);i*j<=b;j++){
			used[i*j-a]=1;
		}
	}
	for (ll i=0;i<=b-a;i++){//最后来统计
		if (used[i]==0){
			cnt++;
			primes[cnt]=i+a;//如果需要的话,记得加上a
		}
	}
}

质数筛的未知代码

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

long long n, p[340000], v[340000];

void solve(){
	long long i,j,m;
    for(m=1;m*m<=n;m++){
        p[m]=n/m-1;
	}
    for(i=1;i<=m;i++){
    	v[i]=i-1;
	}
    for(i=2;i<=m;i++){
        if(v[i]==v[i-1]) continue;
        for(j=1;j<=min(m-1,n/i/i);j++){
            if(i*j<m) p[j]-=p[i*j]-v[i-1];
            else p[j]-=v[n/i/j]-v[i-1];
		}
        for(j=m;j>=i*i;j--){
            v[j]-=v[j/i]-v[i-1];
		}
    }
}

int main(){
    while(scanf("%lld",&n)!=EOF){
        solve();
        printf("%lld\n",p[1]);
    }
    return 0;
}

这是一份非常牛逼的代码,根据洛谷程名大佬yangyafan的推测,这个可能是杜教筛,时间复杂度在 O ( n 2 3 ) O(n^{\frac{2}{3}}) O(n32) 左右,博主正在努力的搞懂代码的意思,可能后期会有更新。不过大家现在还是可以先当模板用着。


质因数分解

正常的暴力

这个知识相比与之前的就要简单很多了,我们在文章的开头,提到了关于素数的两个定理,其中一个就是算数基本定理 。算数基本定理指任何一个大于1的正整数都能唯一分解为有限个质数的乘积。那么我们这里结合判定质数的试除法,扫描 1 → n 1 \to \sqrt n 1→n 中的 每个质数 d d d ,若 d d d 能够整除 n n n ,则从现在的 n n n 中剔除所以的值为 d d d 的质因子,并记录剔除的个数。

注意:如果操作完毕后,最终剩下的数不为1,则这个数一定是一个质数,且为原本的 n n n 的质因子,所以也要计入结果。

这样的时间复杂度在外层循环和内层循环的拉扯之下,成功的来到了 O ( n ) O(\sqrt n) O(n ) 。

cpp 复制代码
const int INF=1e5+10;
int m;
int prime[INF],c[INF];
void divide(int n){
	m=0;
	for (int i=2;i*i<=n;i++){
		if (n%i==0){
			m++;
			prime[m]=i,c[m]=0;
			while (n%i==0){
				n/=i;
				c[m]++;
			}
		}
	}
	if (n>1){
		m++;
		prime[m]=n;
		c[m]=1;
	}
	for (int i=1;i<=m;i++){
		cout<<prime[i];
		if (c[i]!=1)cout<<"^"<<c[i];
		if (i!=m)cout<<"*";
	}
}

Pollard-Rho算法

你觉得我会讲吗?请自行脑补。

这种算法的时间复杂度达到了惊人的 O ( n 1 4 ) O(n^{\frac{1}{4}}) O(n41) ,可谓是快的不要不要的。


参考文献:

百度百科

若喜欢,请留下你的收藏与点赞吧,如有问题请直接评论,博主会尽量解决。

相关推荐
重生之我是数学王子3 分钟前
QT基础 编码问题 定时器 事件 绘图事件 keyPressEvent QT5.12.3环境 C++实现
开发语言·c++·qt
℘团子এ3 分钟前
vue3中如何上传文件到腾讯云的桶(cosbrowser)
前端·javascript·腾讯云
Ai 编码助手5 分钟前
使用php和Xunsearch提升音乐网站的歌曲搜索效果
开发语言·php
学习前端的小z9 分钟前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
神仙别闹16 分钟前
基于C#和Sql Server 2008实现的(WinForm)订单生成系统
开发语言·c#
XINGTECODE17 分钟前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
alphaTao20 分钟前
LeetCode 每日一题 2024/11/18-2024/11/24
算法·leetcode
kitesxian29 分钟前
Leetcode448. 找到所有数组中消失的数字(HOT100)+Leetcode139. 单词拆分(HOT100)
数据结构·算法·leetcode
前端百草阁32 分钟前
【TS简单上手,快速入门教程】————适合零基础
javascript·typescript
彭世瑜33 分钟前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript