埃氏筛法
核心思想:对于任意一个大于1的整数,它的 k (k > 1) 倍是一个合数。
怎么做呢? 用质数的 k 倍筛合数。
具体流程:我们可以从2开始遍历到n,遍历每一个数字 i 时,如果 i 没有被标记(i 是质数),那么就从小到大枚举它的 k (k > 1) 倍的数字(最大也不要超过 n),打上合数的标记。
小优化1:就是在枚举质数 i 的 k 倍数字时,可以从 i * i 开始枚举,因为小于 i * i 的数字都已经被枚举过了。
举个例子🌰:
当 i 从 2 枚举到 8 的时候,现在我们要开始筛掉所有 2~n 范围内 8 的倍数,将它们标记为合数。那么现在问题来了:要从 1 * 8、2 * 8、3 * 8 开始一直枚举吗?并不需要。如图所示,我们只需要从 8 * 8 开始枚举,因为小于 8 * 8 的倍数(7 * 8 、6 * 8、5 * 8、4 * 8 …)都已经在 i 到达 8 之前枚举过了。
模板代码:
void get_prime()
{
for(LL i=2;i<=n;i++) //开LL防溢出
{
if(!st[i]) //是质数
{
for(LL j=i*i;j<=n;j+=i) //开LL 小优化:从i*i开始遍历
{
st[j] = true;
}
}
}
}
特点:根据代码我们可以看出来,埃氏筛法使用已经确定的质数去筛掉合数,遇到合数会直接跳过。
小优化2:任何一个合数 a 的两个因数必定一个大于 sqrt(a),另一个小于 sqrt(a) 。
于是我们在枚举 i 的时候,只用枚举到 sqrt(n) 即可。
代码如下:
void get_prime1()
{
for(LL i=2;i<=n/i;i++) //只枚举到sqrt(n),将所有的合数打上标记 i<=n/i是防溢出写法
{
if(!st[i]) //是质数
{
for(LL j=i*i;j<=n;j+=i)
{
st[j] = true;
}
}
}
时间复杂度
O(nloglogn)
loglogn已经近似常数了,算是非常优的算法,但是埃氏筛法在标记合数的时候可能会重复多次访问同一个数字,如果我们能让每一个数字只被访问一次,那么时间复杂度就会降低到O(n),于是我们在线性筛里要解决这个问题。
线性筛法(欧拉筛法)
核心思想:要让一个合数被它的最小质因数筛掉。
怎么实现这个想法呢?
首先我们需要一个 p 数组将当前找到的质数都记录下来,因为我们要让一个合数被它的最小质因数筛掉。
然后我们从小到大遍历 2~n 的所有数字 i ,如果遇到没被标记的数字就加入质数数组 p ,然后通过 j 遍历质数数组,将 i 从小到大依次乘上当前找到的所有质数,然后筛掉这个乘积。当 j 遍历到某个质数,i 可以整除这个质数的时候就停止当前的 i 的枚举,进入到下一个 i 的枚举。
与埃氏筛法不同的氏,埃氏筛法所有筛掉的合数都是通过质数筛掉的;而线性筛法晒掉的合数不一定都是由质数筛掉的,合数也会参与筛合数。
总结:
从前往后遍历每一个 i
- 用 i 的质数倍去筛
- 遇到 i 是质数的倍数的时候就要停止
😕为什么遇到某一个质数可以整除 i 的时候就要停止呢?
简单理解就是当你遇到某一个质数,你是它的倍数的时候,这个时候筛掉的合数仍然是可以看作被这个最小质因数筛掉的。但是如果继续筛,你身上就携带了这个可以整除你的质数。
就拿图中的例子来说,遇到 9 * 3 的时候,由于 9 是 3 的倍数,于是我可以将 9 分解成 3 * 3 ,于是 9 * 3 就变成了 3 * 3 * 3(27),这个 27 就是被它的最小质因数筛掉的,但是如果继续筛 9 * 4 ,那么 36 就是被 4 这个质数筛掉的,但是换个视角看,9 * 4 可以分解为 3 * 3 * 4 ,最小的质数是 3,所以 36 实际上在将来是应该要被它的最小质因数 3 筛掉的,即 12 * 3 的时候筛掉这个 36。这样以来就导致了重复的访问。
模板代码
void get_prime()
{
for(LL i=2;i<=n;i++) //LL是防止 i * p[j]的时候溢出
{
if(!st[i]) p[++cnt] = i; //没有标记就是质数,加入质数的数组
for(int j=1;i * p[j] <= n;j++) //j是p数组的下标,依次筛去i的质数倍
{
st[i * p[j]] = true; //合数打上标记,筛掉
if(i % p[j] == 0) break; //如果i是该质数的倍数就停止
}
}
}
特性:
if(i % p[j] == 0) break;
这个判定条件能让每一个合数被自己的最小质因数筛掉。
- 如果 i 是合数,枚举到最小质因数的时候跳出循环
- 如果 i 是质数,枚举到自身时跳出循环
注意,在筛的过程中,我们还能知道 p[j] 是 i 的最小质因数
模板题
ac代码
#include<iostream>
using namespace std;
typedef long long LL;
const int N = 1e8 + 10;
int n,q;
bool st[N];
int p[N],cnt;
//埃氏筛
void get_prime1()
{
for(LL i=2;i<=n;i++)
{
if(!st[i]) //是质数
{
p[++cnt] = i;
for(LL j=i*i;j<=n;j+=i)
{
st[j] = true;
}
}
}
}
//只枚举到sqrt(n)的埃氏筛 因为题目要求第k小的质数,所以还要单独遍历一遍st数组,似优化又好像非优化,都差不多
void get_prime1()
{
for(LL i=2;i<=n/i;i++) //先枚举到sqrt(n),将所有的合数打上标记
{
if(!st[i]) //是质数
{
for(LL j=i*i;j<=n;j+=i)
{
st[j] = true;
}
}
}
for(int i=2;i<=n;i++) //再从2开始遍历整个2~n范围,将没有标记的数字加入质数数组
if(!st[i]) p[++cnt] = i;
}
//线性筛
void get_prime2()
{
for(LL i=2;i<=n;i++) //LL是防止 i * p[j]的时候溢出
{
if(!st[i]) p[++cnt] = i; //没有标记就是质数,加入质数的数组
for(int j=1;i * p[j] <= n;j++) //j是p数组的下标,依次筛去i的质数倍
{
st[i * p[j]] = true; //合数打上标记,筛掉
if(i % p[j] == 0) break; //如果i是该质数的倍数就停止
}
}
}
int main()
{
scanf("%d%d",&n,&q);
get_prime1();
while(q--)
{
int k; cin >> k;
printf("%d\n",p[k]);
}
return 0;
}