线性筛素数

题目连接:P3383 【模板】线性筛素数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

 前提:

  1. prime number:素数 
  2.  每一个合数都可以表示为若干个质数之积
  3. 最小质因数 × 最大因数(非自己) = 这个合数”

想要快速地筛出一定上限内的素数?

下面这种方法可以保证范围内的每个合数都被删掉(在 bool 数组里面标记为非素数),而且任一合数只被:

“最小质因数 × 最大因数(非自己) = 这个合数”

的途径删掉。由于每个数只被筛一次,时间复杂度为 O(n)O(n)。

欧拉筛

先浏览如何实现再讲其中的原理。

实现

#include <cstdio>
#include <cstring>

bool isPrime[100000010];
//isPrime[i] == 1表示:i是素数
int Prime[6000010], cnt = 0;
//Prime存质数

void GetPrime(int n)//筛到n
{
	memset(isPrime, 1, sizeof(isPrime));
	//以“每个数都是素数”为初始状态,逐个删去
	isPrime[1] = 0;//1不是素数
	
	for(int i = 2; i <= n; i++)
	{
		if(isPrime[i])//没筛掉 
			Prime[++cnt] = i; //i成为下一个素数
			
		for(int j = 1; j <= cnt && i*Prime[j] <= n/*不超上限*/; j++) 
		{
        //从Prime[1],即最小质数2开始,逐个枚举已知的质数,并期望Prime[j]是(i*Prime[j])的最小质因数
        //当然,i肯定比Prime[j]大,因为Prime[j]是在i之前得出的
			isPrime[i*Prime[j]] = 0;
            
			if(i % Prime[j] == 0)//i中也含有Prime[j]这个因子
				break; //重要步骤。见原理
		}
	}
}

int main()
{
	int n, q;
	scanf("%d %d", &n, &q);
	GetPrime(n);
	while (q--)
	{
		int k;
		scanf("%d", &k);
		printf("%d\n", Prime[k]);
	}
	return 0;
}


原理概述

代码中,外层枚举 i = 1 \to ni=1→n。对于一个 ii,经过前面的腥风血雨,如果它还没有被筛掉,就加到质数数组 Prime[]Prime[] 中。下一步,是用 ii 来筛掉一波数。

内层从小到大枚举 Prime[j]Prime[j]。i×Prime[j]i×Prime[j] 是尝试筛掉的某个合数,其中,我们期望 Prime[j]Prime[j] 是这个合数的最小质因数 (这是线性复杂度的条件,下面叫做“筛条件”)。它是怎么得到保证的?

jj 的循环中,有一句就做到了这一点:

			if(i % Prime[j] == 0)
				break; 

jj 循环到 i \mod Prime[j] == 0imodPrime[j]==0 就恰好需要停止的理由是:

  • 下面用 s(smaller)s(smaller) 表示小于 jj 的数,L(larger)L(larger) 表示大于 jj 的数。

  • ① ii 的最小质因数肯定是 Prime[j]Prime[j]。

    (如果 ii 的最小质因数是 Prime[s]Prime[s] ,那么 Prime[s]Prime[s] 更早被枚举到(因为我们从小到大枚举质数),当时就要break)

    既然 ii 的最小质因数是 Prime[j]Prime[j],那么 i × Prime[j]i×Prime[j] 的最小质因数也是 Prime[j]Prime[j]。所以,jj 本身是符合“筛条件”的。

  • ② i × Prime[s]i×Prime[s] 的最小质因数确实是 Prime[s]Prime[s]。

    (如果是它的最小质因数是更小的质数 Prime[t]Prime[t],那么当然 Prime[t]Prime[t] 更早被枚举到,当时就要break)

    这说明 jj 之前(用 i × Prime[s]i×Prime[s] 的方式去筛合数,使用的是最小质因数)都符合“筛条件”。

  • ③ i × Prime[L]i×Prime[L] 的最小质因数一定是 Prime[j]Prime[j]。

    (因为 ii 的最小质因数是 Prime[j]Prime[j],所以 i × Prime[L]i×Prime[L] 也含有 Prime[j]Prime[j] 这个因数(这是 ii 的功劳),所以其最小质因数也是 Prime[j]Prime[j](新的质因数 Prime[L]Prime[L] 太大了))

    这说明,如果 jj 继续递增(将以 i × Prime[L]i×Prime[L] 的方式去筛合数,没有使用最小质因数),是不符合“筛条件”的。

小提示:

当 ii 还不大的时候,可能会一层内就筛去大量合数,看上去耗时比较大,但是由于保证了筛去的合数日后将不会再被筛(总共只筛一次),复杂度是线性的。到 ii 接近 nn 时,每层几乎都不用做什么事。

建议看下面两个并不复杂的证明,你能更加信任这个筛法,利于以后的扩展学习。

正确性(所有合数都会被标记)证明

设一合数 CC(要筛掉)的最小质因数是 p_1p1​,令 B = C / p_1B=C/p1​(C = B × p_1C=B×p1​),则 BB 的最小质因数不小于 p_1p1​(否则 CC 也有这个更小因子)。那么当外层枚举到 i = Bi=B 时,我们将会从小到大枚举各个质数;因为 i = Bi=B 的最小质因数不小于 p_1p1​,所以 ii 在质数枚举至 p_1p1​ 之前一定不会break,这回,CC 一定会被 B × p_iB×pi​ 删去。

核心:亲爱的 BB 的最小质因数必不小于 p_1p1​。

例:315 = 3 × 3 × 5 × 7315=3×3×5×7,其最小质因数是 33。考虑 i = 315 / 3 = 105i=315/3=105 时,我们从小到大逐个枚举质数,正是因为 ii 的最小质因数不会小于 33(本例中就是 33),所以当枚举 j = 1 (Prime[j] = 2)j=1(Prime[j]=2) 时,ii 不包含 22 这个因子,也就不会break,直到 Prime[j] = 3Prime[j]=3 之后才退出。

当然质数不能表示成“大于1的某数×质数”,所以整个流程中不会标记。

线性复杂度证明

注意这个算法一直使用“某数×质数”去筛合数,又已经证明一个合数一定会被它的最小质因数 p_1p1​ 筛掉,所以我们唯一要担心的就是同一个合数是否会被“另外某数 × p_1p1​ 以外的质数”再筛一次导致浪费时间。设要筛的合数是 CC,设这么一个作孽的质数为 p_xpx​,再令 A = C / p_xA=C/px​,则 AA 中一定有 p_1p1​ 这个因子。当外层枚举到 i = Ai=A,它想要再筛一次 CC,却在枚举 Prime[j] = p_1Prime[j]=p1​ 时,因为 i \mod Prime[j] == 0imodPrime[j]==0 就退出了。因而 CC 除了 p_1p1​ 以外的质因数都不能筛它。

核心:罪恶的 AA 中必有 p_1p1​ 这个因子。

例:315 = 3 × 3 × 5 × 7315=3×3×5×7。首先,虽然看上去有两个 33,但我们筛数的唯一一句话就是

			isPrime[i*Prime[j]] = 0;

所以,315315 只可能用 105 × 3105×3 或 63 × 563×5 或 45 × 745×7 这三次筛而非四次。然后,非常抱歉,后两个 i = 63, i = 45i=63,i=45 都因为贪婪地要求对应的质数 Prime[j]Prime[j] 为 55 、77,而自己被迫拥有 33 这个因数,因此他们内部根本枚举不到 55 、77,而是枚举到 33 就break了。

以上两个一证,也就无可多说了。


posted @ 2022-05-05 08:42  光風霽月  阅读(18)  评论(0编辑  收藏  举报