质数筛
前言
这一篇博客主要是对数学知识里面的质数部分的常见问题及其用到的方法的透析和扩展,主要的便是对质因数分解的分析,这其实就是质数筛的变化。
由于这部分内容比较容易理解,所以我们用讲解例题的方式打开认识质数的大门。
例题.1 质因数分解 “平凡做法”
题目链接奉上**https://www.acwing.com/problem/content/868/**
相似例题<https://www.acwing.com/problem/content/869/>
题目描述
给定 n 个正整数 ai,判定每个数是否是质数。
输入格式
第一行包含整数 n,
接下来 n 行,每行包含一个正整数 ai。
输出格式
共 n行,其中第 i 行输出第 i 个正整数 ai 是否为质数,是则输出 **Yes**
,否则输出 **No**
。
数据范围
1 ≤ n ≤ 100,
1 ≤ ai ≤ 2^31−1,
想必大家对质因数分解的最浅层理解就是将所有小于n的数进行整除判断n是否是质数,代码比较简单,以下代码仅作示例
void shai(int m)
{
if (m == 2)
{
cout << "Yes" << endl;
return;
}
else if(m==1)
{
cout<<"No"<<endl;
return;
}
for (int i = 2; i <= sqrt(m) + 1; i++)
{
if (m % i == 0)
{
cout << "No" << endl;
return;
}
}
cout << "Yes" << endl;
return;
}
**!!**判断质数时要注意的是对n做出特殊判断,n为1时要做特殊判断。
完整代码奉上
#include<iostream>
#include<cmath>
using namespace std;
void shai(int m)
{
if (m == 2)
{
cout << "Yes" << endl;
return;
}
else if(m==1)
{
cout<<"No"<<endl;
return;
}
for (int i = 2; i <= sqrt(m) + 1; i++)
{
if (m % i == 0)
{
cout << "No" << endl;
return;
}
}
cout << "Yes" << endl;
return;
}
int main()
{
int n;
cin >> n;
for (int i = 0; i < n; i++)
{
int l;
cin >> l;
shai(l);
}
return 0;
}
学习到这里,大家都应该很轻松,但是这是分解质因数最普通最耗时的做法了,时间复杂度相当于是O(n^2) ps::在对n个数进行判断时.
那有没有更便捷优化的方法?
那肯定是有的啦,而且不止一种,下面我就来介绍两种常见的质数筛优化做法,
Eratosthenes埃式筛
埃式筛的原理相当于是对于一个数n,将其1~n范围之内的每一个质数进行后续的标记处理直到这个数大于等于n为止,在对n范围内足够多的数遍历后最终会覆盖整个n范围内,筛出所有质数。
2 | 2*2 | 2*3 | … | 2*m<n |
---|---|---|---|---|
3 | 3*2 | 3*3 | … | 3*m |
5 | 5*2 | 5*3 | … | 4*m |
做法是开一个一维数组V[ i ]用于判断这个数是否被标记,标记的就不是质数,反之则为质数,下面为示例代码
for(int i=2;i<=n;i++)
{
if(v[i]) continue;//如果v[i]不等于0,那么i就不是质数,直接跳过后续操作
for(int j=1;j*i<=n;j++)
v[j*i]=1;
}
按如上方法就可以筛出n内的所有质数,埃式筛其实已经很接近线性的做法了,但其究不是线性表,快捷是不及线性做法的,其实仔细观察可以发现,对2和3往后筛的时候,2*3和 3*2都是6,即对6重复标记了这样就进行了多余的操作了,多耗时间。
接下来要介绍的方法就是大大大大大大大大大名鼎鼎的欧拉筛
写这么多大其实没什么用哈哈,就是夜深了有点无聊。。。。。
欧拉筛
下面简单介绍一下欧拉筛
欧拉筛也是通过标记的方法筛出质数,不过与埃式筛不同的是欧拉筛标记的是每一个数的最小分解因数,且标记的方法是标记每一个小于该质数的质数与该质数相乘所得到的数字,光从这段话可能不好理解,但是看下面的图表就会明朗
数n | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|
小于n的质数(且小于等于n的最小质因数) | 2 | 2,3 | 2 | 2,3,5 | 2 | 2,3,5 | 2 | 2,3 |
即将要标记的数 | 2,4 | 3,6,9 | 8 | 5,10,15,25 | 6,12 | 7,14,21,35 | 16 | 18,27 |
可以看到遍历到 9 的时候在小于等于9的数字 都遍历了一遍 而且 只 遍历了一遍 ,往后的大于9的数字也是一样的 |
---|
这里先给出区域块功能代码
for(int i=2;i<=n;i++)
{
if(a[i]==0) a[i]=i,b[i]=1,prime[++m]=i;
for(int j=1;j<=m;j++)
{
if(prime[j]>a[i]||prime[j]>n/i) break;
a[prime[j]*i]=prime[j];
}
}
这部分代码之中呢 a[ i ] 是记录第 i 个数的最小质因数,暂时不用管B数组,prime数组是记录的从2开始的所有质数;
光从代码上看,你可能会认为这个方法的时间复杂度是 N^2,但是通过上面图解表格分析推理之后,我们发现其实每一个数字都只被记录了一遍
所以一共进行了n次标记,则时间复杂度是 N,证毕这即是线性做法.
我想大家的手已经蠢蠢欲动了,既然学习了新的方法,那么就应该思考如何在新的问题上应用它,下面分享一道比较简单的题目练练手(来自acwing)
例题.2 筛质数
题目链接:https://www.acwing.com/problem/content/870/
题目描述
给定一个正整数 n,请你求出 1∼n 中质数的个数,
输入格式
共一行,包含整数 n,
输出格式
共一行,包含一个整数,表示 1∼n 中质数的个数。
数据范围
1≤ n ≤10^6,
以下代码作为示例:其他更好的做法可以在评论区分享出来
#include<iostream>
using namespace std;
const int N=1e6+1;
int a[N],prime[N],b[N];
int main()
{
int n;
cin>>n;
int m=0;
for(int i=2;i<=n;i++)
{
if(a[i]==0) a[i]=i,b[i]=1,prime[++m]=i;
for(int j=1;j<=m;j++)
{
if(prime[j]>a[i]||prime[j]>n/i) break;
a[prime[j]*i]=prime[j];
}
}
for(int i=1;i<=n;i++) b[i]+=b[i-1];
cout<<b[n]<<endl;
return 0;
}
希望可以帮助到大家…kmj,