如何快速筛出质数?
前言
有时我们想筛出一定范围内的质数。
朴素方法
假如我们要求 \([2,n]\) 内的所有质数:
- 遍历 \(2\le i\le n\),判断 \(i\) 是否是质数:
- 如果 \(\exists~2\le j\le\sqrt{i}\) 使得 \(j|i\),那么 \(i\) 不是质数。
但这样明显复杂度是 \(O(n\sqrt{n})\),无法达到最佳水平。
埃氏筛
一个更聪明的方法是枚举每个数并将它的倍数筛去。
可以证明,这样的复杂度是 \(O(n\log\log n)\) 的。
证明:
如果每一次对数组的操作花费 1 个单位时间,则时间复杂度为:
\[O\left(\sum_{k=1}^{\pi(n)}{n\over p_k}\right)=O\left(n\sum_{k=1}^{\pi(n)}{1\over p_k}\right) \]其中 \(p_k\) 表示第 \(k\) 小的素数,\(\pi(n)\) 表示 \(\le n\) 的素数个数。\(\sum_{k=1}^{\pi(n)}\) 表示第一层 for 循环,其中累加上界 \(\pi(n)\) 为
if (prime[i])
进入 true 分支的次数;\(n\over p_k\) 表示第二层 for 循环的执行次数。根据 Mertens 第二定理,存在常数 \(B_1\) 使得:
\[\sum_{k=1}^{\pi(n)}{1\over p_k}=\log\log n+B_1+O\left(1\over\log n\right) \]所以 Eratosthenes 筛法 的时间复杂度为 \(O(n\log\log n)\)。接下来我们证明 Mertens 第二定理的弱化版本 \(\sum_{k\le\pi(n)}1/p_k=O(\log\log n)\):
根据 \(\pi(n)=\Theta(n/\log n)\),可知第 \(n\) 个素数的大小为 \(\Theta(n\log n)\)。于是就有
\[\begin{aligned} \sum_{k=1}^{\pi(n)}{1\over p_k} &=O\left(\sum_{k=2}^{\pi(n)}{1\over k\log k}\right) \\ &=O\left(\int_2^{\pi(n)}{\mathrm dx\over x\log x}\right) \\ &=O(\log\log\pi(n))=O(\log\log n) \end{aligned} \]当然,上面的做法效率仍然不够高效,应用下面几种方法可以稍微提高算法的执行效率。——OI Wiki
线性筛(欧拉筛)
重头戏来了:如果我们想把时间复杂度优化到 \(O(n)\),怎么办?
\(\texttt{Update 2024.1.6}\) 感觉之前讲得不是很清楚。于是新增解释:
- 设当前遍历的数为 \(i\),遍历之前筛过的所有质数 \(p_j\),试图使用 \(p_j\times i\) 筛数。
首先建立一个认识:\(p_j\) 是一个质数,\(i=p_{k(1)}\times\cdots\times p_{k(n)},p_{k(i)}<p_{k(i+1)}\),很可能是很多质数相乘。 - 将 \(p_j\times i\) 标记为合数。
- 如果 \(p_j|i\),
break;
,枚举 \(i+1\)。因为 \(p_j|i\) 说明 \(i=p_{k(1)}\times\cdots\times p_j \times\cdots\times p_{k(n)}\),即 \(i\) 的质因数中出现了 \(p_j\)。
同时因为遍历到了 \(p_j\),所以之前的 \(p_k,k<j\) 都没有满足 \(p_k|i\),所以上式中 \(k(1)=j,p_{k(1)}=p_j\),也就是说 \(p_j\) 是 \(i\) 的最小质因子,那么之后的 \(p_{j+1},p_{j+2},\cdots\) 都将不是 \(i\) 的最小质因子。
所以这样就做到了 \(i\) 都只因为最小质因子 \(p_{k(1)}\) 而筛去,满足了线性。
备注:下面这段是更新前的解释。可以选阅。
依然采用以 \(i\) 筛 \(i\times j\) 的方法,但想要做到线性,必须满足 \(i\times j\) 只被筛去一次。
我们规定当筛去 \(i\times j\) 时,\(i\) 必须是 \(i\times j\) 的最小质因子,因为一个数的最小质因子是唯一的,所以 \(i\times j\) 只会被筛一次。
依然遍历 \(2\le i\le n\),遍历之前筛出的所有质数 \(j_k\),因为 \(j_k\) 是质数,所以 \(j_k\) 才有可能是 \(i\times j_k\) 的最小质因子。当 \(i\times j\) 的最小质因子不是 \(j\) 时,break;
,遍历下一个 \(i\)。
(下文用 \(j_k\) 表示第 \(k\) 个 \(j\)。)
那么什么时候 \(j_k\) 不是 \(i\times j_k\) 的最小质因子呢?当 \(j_{k-1}|i\) 时。因为此时 \(j_{k-1}\) 是 \(i\) 的因数,之前的 \(j_l,(l<k-1)\) 都不是 \(i\) 的因数(否则就会遍历下一个 \(i\) 去了),所以 \(j_{k-1}\) 是 \(i\) 的最小质因子,因为 \(j_{k-1}<j_k\),所以 \(i\times j_k\) 的最小质因子是 \(i\) 中的 \(j_{k-1}\),而非 \(j_k\)。于是 break;
,遍历下一个 \(i\)。
转换一下,就是筛完 \(i\times j_k\) 之后,判断是否 \(j_k|i\),是就 break;
,否则遍历 \(j_{k+1}\)。
代码
#include <iostream>
#include <bitset>
#include <vector>
using namespace std;
int main()
{
int n,q;
scanf("%d %d",&n,&q);
bitset<100000001> isp;
vector<int> p;
isp.set();
isp[0]=false;
isp[1]=false;
for(int i=2;i<=n;i++)
{
if(isp[i])
{
p.push_back(i);
}
int l=p.size();
for(int j=0;j<l&&p[j]*i<=n/* 防越界 */;j++)
{
isp[p[j]*i]=false;
if(i%p[j]==0)// 如果整除,说明 p[j] 是 i 的最小质因子,再往下筛就重复了
break;// 所以 break
}
}
int k;
while(q--)
{
scanf("%d",&k);
printf("%d\n",p[k-1]);
}
return 0;
}
后记
完结撒花!