素数欧拉筛法证明
转载自https://www.luogu.com.cn/blog/cicos/notprime!!!
2020-02-01 更新
想要快速地筛出一定上限内的素数?
下面这种方法可以保证范围内的每个合数都被删掉(在 bool 数组里面标记为非素数),而且任一合数只被:
“最小质因数 × 最大因数(非自己) = 这个合数”
的途径删掉。由于每个数只被筛一次,时间复杂度为 。
欧拉筛
先浏览如何实现再讲其中的原理。
实现
#include <cstdio>
#include <cstring>
bool isPrime[100000010];
//isPrime[i] == 1表示:i是素数
int Prime[5000010], cnt = 0;
//Prime存质数
void GetPrime(int n)//筛到n
{
memset(isPrime, 1, sizeof(isPrime));
//以“每个数都是素数”为初始状态,逐个删去
isPrime[1] = 0;//1不是素数
<span class="hljs-keyword">for</span>(<span class="hljs-keyword">int</span> i = <span class="hljs-number">2</span>; i <= n; i++)
{
<span class="hljs-keyword">if</span>(isPrime[i])<span class="hljs-comment">//没筛掉 </span>
Prime[++cnt] = i; <span class="hljs-comment">//i成为下一个素数</span>
<span class="hljs-keyword">for</span>(<span class="hljs-keyword">int</span> j = <span class="hljs-number">1</span>; j <= cnt && i*Prime[j] <= n<span class="hljs-comment">/*不超上限*/</span>; j++)
{
<span class="hljs-comment">//从Prime[1],即最小质数2开始,逐个枚举已知的质数,并期望Prime[j]是(i*Prime[j])的最小质因数</span>
<span class="hljs-comment">//当然,i肯定比Prime[j]大,因为Prime[j]是在i之前得出的</span>
isPrime[i*Prime[j]] = <span class="hljs-number">0</span>;
<span class="hljs-keyword">if</span>(i % Prime[j] == <span class="hljs-number">0</span>)<span class="hljs-comment">//i中也含有Prime[j]这个因子</span>
<span class="hljs-keyword">break</span>; <span class="hljs-comment">//重要步骤。见原理</span>
}
}
}
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;
}
原理概述
代码中,外层枚举 。对于一个 ,经过前面的腥风血雨,如果它还没有被筛掉,就加到质数数组 中。下一步,是用 来筛掉一波数。
内层从小到大枚举 。 是尝试筛掉的某个合数,其中,我们期望 是这个合数的最小质因数 (这是线性复杂度的条件,下面叫做“筛条件”)。它是怎么得到保证的?
的循环中,有一句就做到了这一点:
if(i % Prime[j] == 0)
break;
循环到 就恰好需要停止的理由是:
-
下面用 表示小于 的数, 表示大于 的数。
-
① 的最小质因数肯定是 。
(如果 的最小质因数是 ,那么 更早被枚举到(因为我们从小到大枚举质数),当时就要break)
既然 的最小质因数是 ,那么 的最小质因数也是 。所以, 本身是符合“筛条件”的。
-
② 的最小质因数确实是 。
(如果是它的最小质因数是更小的质数 ,那么当然 更早被枚举到,当时就要break)
这说明 之前(用 的方式去筛合数,使用的是最小质因数)都符合“筛条件”。
-
③ 的最小质因数一定是 。
(因为 的最小质因数是 ,所以 也含有 这个因数(这是 的功劳),所以其最小质因数也是 (新的质因数 太大了))
这说明,如果 继续递增(将以 的方式去筛合数,没有使用最小质因数),是不符合“筛条件”的。
小提示:
当 还不大的时候,可能会一层内就筛去大量合数,看上去耗时比较大,但是由于保证了筛去的合数日后将不会再被筛(总共只筛一次),复杂度是线性的。到 接近 时,每层几乎都不用做什么事。
建议看下面两个并不复杂的证明,你能更加信任这个筛法,利于以后的扩展学习。
正确性(所有合数都会被标记)证明
设一合数 (要筛掉)的最小质因数是 ,令 ( ),则 的最小质因数不小于 (否则 也有这个更小因子)。那么当外层枚举到 时,我们将会从小到大枚举各个质数;因为 的最小质因数不小于 ,所以 在质数枚举至 之前一定不会break,这回, 一定会被 删去。
核心:亲爱的 的最小质因数必不小于 。
例: ,其最小质因数是 。考虑 时,我们从小到大逐个枚举质数,正是因为 的最小质因数也不会小于 (本例中就是 ),所以当枚举 时, 不包含 这个因子,也就不会break,直到 之后才退出。
当然质数不能表示成“大于1的某数×质数”,所以整个流程中不会标记。
线性复杂度证明
注意这个算法一直使用“某数×质数”去筛合数,又已经证明一个合数一定会被它的最小质因数 筛掉,所以我们唯一要担心的就是同一个合数是否会被“另外某数 × 以外的质数”再筛一次导致浪费时间。设要筛的合数是 ,设这么一个作孽的质数为 ,再令 ,则 中一定有 这个因子。当外层枚举到 ,它想要再筛一次 ,却在枚举 时,因为 就退出了。因而 除了 以外的质因数都不能筛它。
核心:罪恶的 中必有 这个因子。
例: 。首先,虽然看上去有两个 ,但我们筛数的唯一一句话就是
isPrime[i*Prime[j]] = 0;
所以, 只可能用 或 或 这三次筛而非四次。然后,非常抱歉,后两个 都因为贪婪地要求对应的质数 为 、 ,而自己被迫拥有 这个因数,因此他们内部根本枚举不到 、 ,而是枚举到 就break了。
以上两个一证,也就无可多说了。
更新日志:
2019-02-22 原理简化;用词修改或订正。
2019-04-02 一些用词更准确;加入更多括号内的注释,减少回看上文的需要。
2020-02-01 题面修改了,补充一下答案输出。
如有错误还请大佬们多多指教!