【数论】素数筛法

素数筛法

一.前言

素数是数论中颇为重要的一类数,我们往往需要对其进行判断。当我们需要寻找一个大范围内的所有素数时,如何筛选这些素数就显得非常重要了。本文中介绍了三种筛法:朴素判定,埃拉托色尼筛法,欧拉筛法。

二.朴素判定法

当我们对一整个范围内的素数进行筛选时,一种朴素的方法很容易想到,那就是对其中每个素数进行判定,最简单的方法就是用试除法,对所有\(<=\sqrt{x}\)的数依次试除,之所以不继续往后,因为乘法交换律,后面的因子都会与前面的因子结合,如果前面判定无因子,后续也不可能再有了。以下是代码实现。

#include<bits/stdc++.h>
using namespace std;
int main(){
    int n;
    scanf("%d",&n);
    for (int i = 2; i <= n; ++i)
    {
        bool isPrime = true;
        for (int j = 2; j * j <= i; ++j)
        {
            if (i%j==0)
            {
                isPrime = false;
                break;
            }

        }
        if(isPrime) printf("%d ",i);
    }
    return 0;
}

每个数\(i\)都需要判定\(\sqrt{i}\)次,筛选\(n\)个数,平均时间复杂度为\(\Theta(n\sqrt{n})\),。明显低效,缺点很明显他朴素的对每个数进行素性判定。而我们在我们在寻找一个范围内所有素数时,由于合数都能被两个小于他的因子相乘,我们是不是能通过这种性质筛去所有合数,而得到剩余的素数呢?

三.埃拉托色尼筛法(埃氏筛)

众所周知,一个素数的倍数是素数,每个合数都能分解成若干个质因数,我们可以借此性质筛去每个素数的倍数,这样留下的必定是素数,因为比该素数小的所有可能因子的倍数已经被筛去。以下是实现代码。

#include<bits/stdc++.h>

using namespace std;
const int N = 1e7;
int isNotPrime[N];

int main() {
    int n;
    scanf("%d", &n);
    for (int i = 2; i <= n; ++i) {
        if (!isNotPrime[i]) {
            printf("%d ", i);
            for (int j = i * i; j <= n; j += i) {
                isNotPrime[j] = 1;
            }
        }
    }
    return 0;
}

这里有几个魔鬼细节。首先我们只对素数进行倍数筛去,因为一个合数必定有小于他的因子,那么他的倍数一定在小于他的因子时筛去,没有必要重复筛。另外内层循环\(j\)是从\(i^2\)开始的,因为\(j\)\(1\rightarrow j-1\)倍已经在筛去\(1 \rightarrow j-1\)的倍数时筛去。每个素数都会执行\(n/2+n/3+n/5...n/k(k为素数)\)次筛去其数值约等于\(n\log_2 \log_2 n\)次,所以埃氏筛时间复杂度约等于\(\Theta (n\log_2 \log_2 n)\)。已经十分逼近线性,他无法达到线性的原因是他出现了重复筛去的问题,例如在对\(5\)进行倍数筛去时,筛去了\(40,50,80...\),而这些数在\(2\)的倍数时已经筛去,这是埃氏筛法无法避免的缺点。

四.欧拉筛(线性筛)

埃氏筛最大的缺点就是会对含有同一个因子的数重复筛去,这会浪费大量时间。欧拉筛,又称欧式筛,他运用和数能拆分成以他的最小素因数与另一个在筛选范围内的数的乘积形式,这一思想,扫描过范围内每一个数,在筛选的同时统计素数,每次筛去已经发现的素数中的素数与每扫描到的数的乘积,当发现扫描到的数与已发现的素数非互素,那么说明扫描到的数的所有倍数均含有这个素数因子,所以无需继续向后与其他素数做乘积,因为那个较小的素数的倍数必定会包含扫描数与后续素数的乘积(因为较小的素数是扫描数的因子)。这可以完美的避免重复筛选,因为他只会筛选到扫描到的数与其最小素因数的乘积,这不会导致某一个重复的因子在一次扫描中被重复利用,也不会导致某一个数被重复筛去,因为他会且仅会被他的最小因子所筛去,而不会被该因子的倍数所筛去

#include<bits/stdc++.h>

using namespace std;
const int N = 1e7;
int isNotPrime[N];
int Prime[N], cnt;

int main() {
    int n;
    scanf("%d", &n);
    for (int i = 2; i <= n; ++i) {
        if (!isNotPrime[i]) {
            printf("%d ", i);
            Prime[++cnt] = i;
        }
        for (int j = 1; j <= cnt; ++j) {
            if (Prime[j] * i > n) break;\\如果超过范围结束。
            isNotPrime[Prime[j] * i] = 1;\\筛去当前扫描数与已发现的素数构成的合数,合数的最小数因子必定是Prime[j],因为素数是按序扫描的。
            if (i % Prime[j] == 0) break;\\该素数是扫描数的因子,那么扫描数与后续素数的乘积必定含有该素因子,后续素数与扫描数的乘积的最小因子必定不是后续素数,而是该素数。
        }
    }
    return 0;
}

欧拉筛对于\(n\)范围每个数扫描一次,在扫描过程中对每个合数进行一次筛去。每个数都会被扫描一次,筛一次,时间复杂度是完全线性的!为\(\Theta(n)\)

五.总结

素数筛法可以在快速的时间内预处理一段范围内的素数,对于小范围需要高频的素数的素数判定的问题起到重要问题。而低频且大范围的素数判定问题,则需要高效的素性测试算法,我会单独出一篇文章,敬请期待。

posted @ 2024-06-02 04:06  Jefferyzzzz  阅读(479)  评论(0)    收藏  举报