[数论+模板] 三大质数筛法(线性筛+素数问题+模板)

1. 朴素埃筛+模板

868. 筛质数

在这里插入图片描述
可参考:[数论基础] 1. 输出素数(素数筛、线性筛、Miller-Rabbin 素性测试、巧妙解法)

思路:

  • 从 2 到 n n n 将每一个数的所有倍数全部筛掉。这样筛过之后剩余的所有数都是质数。因为对于任意一个数 p p p,如果它没被筛掉,那么意味着从 2 到 p − 1 p-1 p1 都没有它的因数,则为质数。
  • 时间复杂度分析: n 2 + n 3 + ⋯ n n = n ( 1 2 + 1 3 + ⋯ 1 n ) \frac n 2 + {\frac n 3}+ \cdots {\frac n n}=n(\frac1 2 + {\frac 1 3}+ \cdots {\frac 1 n}) 2n+3n+nn=n(21+31+n1)。我们可以知道
    1 2 + 1 3 + ⋯ 1 n \frac1 2 + {\frac 1 3}+ \cdots {\frac 1 n} 21+31+n1调和级数,当 n n n 趋于正无穷时,其极限为 l n n + C lnn+C lnn+C,其中 C C C 为欧拉常数。那么朴素筛法的时间复杂度为 O ( n l n n ) O(nlnn) O(nlnn),也可记为 O ( n l o g n ) O(nlogn) O(nlogn),两者同阶。

这种写法是朴素埃筛,时间复杂度稍高,可以对其做优化。

朴素埃筛代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1e6+5;

int n;
int prime[N], cnt;
bool st[N];

void get_primes(int n) {
    for (int i = 2; i <= n; ++i) {
        if (!st[i]) {
            prime[cnt ++] = n;
        }
        
        for (int j = i + i; j <= n; j += i) st[j] = true;
    }
}

int main() {
    cin >> n;
    
    get_primes(n);
    
    cout << cnt << endl;
    
    return 0;
}

在这里插入图片描述

2. 埃筛+模板

这才是真正的埃筛:只筛掉素数的倍数,已被筛过的数就不用再去筛掉它的倍数了。由算术基本定理可知每个数只用被素数筛掉即可。所以可以将 f o r for for 循环放到 i f if if 判断内部。

时间复杂度分析:由质数个数定理 1 到 n n n 当中有 n l n n \frac n {lnn} lnnn 个质数。所以本来调和级数需要计算 n n n 个数,但是现在只需要计算 l n n lnn lnn 个数,所以粗略估计时间复杂度为 O ( n l n n l n n ) = O ( n ) O(\frac {nlnn} {lnn})=O(n) O(lnnnlnn)=O(n)。 但是实际应当 O ( n l n l n n ) O(nlnlnn) O(nlnlnn),可由数学证明。几乎是和 O ( n ) O(n) O(n) 一个级别的。

埃筛模板代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1e6+5;

int n;
int prime[N], cnt;
bool st[N];

void get_primes(int n) {
    for (int i = 2; i <= n; ++i) {
        if (!st[i]) {
            prime[cnt ++] = n;
            for (int j = i + i; j <= n; j += i) st[j] = true;
        }
    }
}

int main() {
    cin >> n;
    
    get_primes(n);
    
    cout << cnt << endl;
    
    return 0;
}

在这里插入图片描述

3. 线性筛+模板

首先谈谈算法速度:

输入 n n n 大约在 1 0 6 10^6 106 时,两者时间复杂度差不多,线性筛稍快。

输入 n n n 大约在 1 0 7 10^7 107 时,线性筛比埃筛快一倍。

差别就在于 l n l n n lnlnn lnlnn


埃筛的不足之处:

  • 例如筛 12 这个数时,其可被因式分解为 12=2*312=3*4,所以当 2 和 3 作为质因子筛掉它们的倍数的时候,会将 12 重复筛掉两次。同理,18 这个数也是这样的。

然而,线性筛保证每一个合数只会被它的最小质因子筛掉

  • 从小到大枚举 prime[j],是存放已经得到的质数数组。
  • 当循环内 if (i % prime[j] == 0) break; 成立时一定能够保证 prime[j]i 的最小质因子,因为是从小到大枚举的。同时 prime[j] 也一定是 prime[j] * i 的最小质因子(想一下算术基本定理分解式),故可以筛掉 prime[j] * i 这个数,且是由它的最小质因子筛掉的。然后直接 break 掉,否则就会出现埃筛的重复情况。
  • i % prime[j] != 0,那么意味着 prime[j] 一定小于 i 的所有质因子,那么 prime[j] 就是 prime[j] * i 这个数的最小质因子,所以用它的最小质因子将其筛掉。

所以算法保证了筛掉的每一个合数都是采用该数的最小质因子进行筛除,即,都是用 st[prime[j] * i] = true; 将其筛掉。

如果当 x 是一个合数,我们来讨论下它一定会被筛掉:

  • 因为 x 为合数,则必存在最小质因子,不妨将其记为 pji 枚举到 x 之前一定会先枚举到 i = x / pj ,那么此时就会在 st[prime[j] * i] = true; 被筛掉,即 st[pj * x / pj] = st[x] = true; 其中 pj 为 x 的最小质因子 故,用 pj 这个 x 的最小素因子,在 x 之前的 x/pj 这个数的时候,就会把 x 提前筛掉。

那么至此证明,每个合数都会被筛掉,且每个合数只会被其最小质因子筛掉,所以每个合数只被筛了一次,那么这个算法就是线性的,时间复杂度为 O ( n ) O(n) O(n)


细节:

在此内层的 for 循环判断条件没必要写成 j < cnt && prime[j] <= n / i,分情况讨论:

  • i 为合数时,prime[j] 枚举到 i 的最小质因子就会 break 掉。
  • i 为质数时,代表没被提前筛过,已经将其加入到了 primes 数组中,那么 prime[j] 枚举到等于 i 时也会停下来,此时两数取模变成 0,则 break
  • 所以无论如何一定会在 j>=cnt 前 停下来,所以不必加上那个限制条件
  • 至于 prime[j] <= n / i 的话,可以理解为它是来控制范围的 st[prime[j] * i] = true; 保证其不越界,同时筛掉 i 的所有现存质因子的倍数。同时循环内部的 if 判断保证了只被最小质因子所筛掉,不会重复筛。

i%primes[j]==0 时必须终止,假设不终止,那么 i∗primes[j+1] 的最小质因子并不是 primes[j+1],如 12=2∗6 最小质因子为 2, 但 12=2*2∗3primes[j+1]=3,显然,最小质因子并不是 3,而是 2,所以找到了最小质因子后就直接将其跳出即可,12 会在 i = 6 时,被 2 筛掉,而不是在 i = 4 时被 3 筛掉。


prime 数组存放小于 n 的所有素数,其个数为 c 个。prime 数组中的素数是递增的,当 i 能整除 prime[j],那么 i * prime[j + 1] 这个合数肯定被 prime[j] 乘以某个数筛掉。因为 i 中含有 prime[j]prime[j]prime[j+1] 小,即 i=k*prime[j],那 i*prime[j+1]=(k*prime[j])*prime[j+1]=k’*prime[j],接下去的素数同理。所以不用筛下去了。因此,在满足 i%prime[j]==0 这个条件之前以及第一次满足改条件时,prime[j] 必定是 prime[j]*i 的最小因子。故不会对一个元素进行重复判断,将效率提升到 O ( n ) O(n) O(n)

有点抽象,可移步 大佬视屏讲解

我以前的博文讲过相关知识点,可参考:[数论基础] 1. 输出素数(素数筛、线性筛、Miller-Rabbin 素性测试、巧妙解法)

线性筛模板代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1e6+5;

int n;
int prime[N], cnt;
bool st[N];

void get_primes(int n) {
    for (int i = 2; i <= n; ++i) {
        if (!st[i]) prime[cnt ++] = i;
        for (int j = 0; prime[j] <= n / i; ++j) {
            st[prime[j] * i] = true;
            if (i % prime[j] == 0) break;
        }
    }
}

int main() {
    cin >> n;
    
    get_primes(n);
    
    cout << cnt << endl;
    
    return 0;
}

在这里插入图片描述

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ypuyu

如果帮助到你,可以请作者喝水~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值