如何快速筛出质数?

发布时间 2023-10-12 19:35:29作者: Po7ed

前言

有时我们想筛出一定范围内的质数。

朴素方法

假如我们要求 \([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)\),怎么办?

依然采用以 \(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}\)

代码

题目:洛谷 P3383 【模板】线性筛素数

#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;
}

后记

完结撒花!