python用方法求区间和 python求区间素数_#include

python用方法求区间和 python求区间素数_python用方法求区间和_02

筛选法

时间限制: 1000 ms    内存限制: 65536 KB

【题目】

输入一个正整数n(10 ≤ n ≤ 2×109),输出n以内质数的数目。

【输入样例】

20

【输出样例】

8

【提示】20以内的质数有2,3,5,7,11,13,17,19

说明

关于质数的基础知识,可以参考上一篇文章:

求单个质数(素数)的算法(入门篇)

本篇主要介绍筛选法。筛选法有多种,而本篇主要介绍埃氏筛选法(埃拉托斯特尼筛选法)的基本原理,与《入门篇》一样,从简单到复杂,不断优化。

题目概述

在入门篇中已讲解,使用求单个质数的枚举算法去求区间质数,如果区间是[2, 2e7以上],超时,也提示应使用筛选法。

筛选法的定义

所谓筛选,顾名思义,就是从一组数据中筛选出目标数据。


python用方法求区间和 python求区间素数_筛选法_03

筛选精细面粉


做糕点需要用到面粉,而做出柔软顺口的糕点,需要用到精细面粉。有时候购买的面粉颗粒大小不一(例如面粉中有结块),可能还会有杂质,需要把颗粒较大的面粉和杂质过滤掉,此时就用到筛子(如右图),筛子底部有一层精密网格。把面粉倒在筛子中,通过不断晃动筛子,精细的面粉就会掉下去,而颗粒较大的还在筛子中。

对于筛选一定范围内的质数,就是运用了这一原理,把一组数组放入筛子中,通过筛选后,可以得到质数。

筛选法的过程

要筛选出一个区间内的质数,首先要使用一维数组装载区间内的正整数。

python用方法求区间和 python求区间素数_筛选法_04

然后通过一定的规则、条件,筛选后剩下的便是质数。

其实,关键就是筛选的规则和条件。

首先要明白,数组的元素不可以删除,不过可以作标记,数组元素的下标表示的是数字本身,而元素的值就是标记。

例如数组a的元素a[2],下标2代表就是数字2本身,而a[2]的值就是标记。

作标记时,主要有两个值,一个代表质数的标记0,一个代表合数标记1。

筛选前,先把数组所有元素的值初始化为0,表示都是质数。

筛选时,根据一定的规则和条件,通过循环把一个区间的合数都标记为1。

筛选后,通过循环,如果某个元素的值为0,则是质数。

筛选方法

筛选的方法是根据质数的定义来选择,与质数相对应的是合数,而一个质数的倍数肯定是合数。

根据这一原理,通过循环遍历一个区间所有整数时,该数的倍数肯定不是质数,而是合数,此时给该合数标记为1。循环结束后,区间内所有合数被标记为1,而剩下的是原来标记为0的质数。

埃氏筛选法(方法1)

外层循环用于遍历2~2e6之间的所有整数,而内层循环是对当前整数i的j倍的合数标记为1。

对于统计质数数目(判断质数语句)是可以写在外层循环中,就不必另写一个循环了。

注意数据类型的选择,因为内层循环的条件是【i * j <= n】,而i * j的最大值会超过int类型的最大值,所以应选long long类型。

#include 
#include 
using namespace std;
bool a[200000001];
int main(){
    long long n = 2e7, cnt = 0;
    int start = clock(); // 开始时间
    for (long long i = 2; i <= n; i++) // 遍历2~2e6之间的整数
    {
        for (long long j = 2; i * j <= n; j++) // j是当前i的倍数
            a[i * j] = 1; // 将合数标记为1
        if (!a[i]) cnt++; // 如果是质数,则计数器+1
    }
    int end = clock(); // 结束时间
    cout <"共有 " <" 个质数" <endl;
    cout <"运行时间:" <1000.0 <"秒";
    return 0;
}

然而,与入门篇的数据量级一样,都是2e6,筛选法运行时间花销约0.195秒。

python用方法求区间和 python求区间素数_筛选法_05

再看《入门篇》的方法4,对比一下,筛选法几乎快了一倍。

python用方法求区间和 python求区间素数_数组_06

可惜遗憾的是,对于2e7以上的量级,还是超时。不过,可以对筛选法进行优化。

python用方法求区间和 python求区间素数_筛选法_07

进一步优化(方法2)

在《入门篇》中,方法4对奇偶数进行优化。同样也适用于筛选法。

因为除了偶数2是质数外,其他的偶数均不是质数。

首先,对于外层循环变量i,直接从3开始,每循环一次递增2。

接着,内层循环变量j,也是从3开始,每循环一次递增2。只有i和j都不是偶数时,它们的乘积才不是偶数。

对于语句【if (!a[i]) cnt++;】,因为i不可能是偶数,所以只是对奇数进行判断。

如此一来,就起到事半功倍的效果。

最后一点需要注意的是,计数器初始化为1,而不是0。因为数字2虽然是偶数,但也是质数。计数器初始为1,就是把数字2先算进去,因为循环是从9开始的。

#include 
#include 
using namespace std;
bool a[200000001];
int main(){
    long long n = 2e7, cnt = 1;
    int start = clock(); // 开始时间
    for (long long i = 3; i <= n; i += 2) // 遍历3~2e6之间的奇数
    {
        for (long long j = 3; i * j <= n; j += 2) // j是当前i的倍数
            a[i * j] = 1; // 将合数标记为1
        if (!a[i]) cnt++; // 如果是质数,则计数器+1
    }
    int end = clock(); // 结束时间
    cout <"共有 " <" 个质数" <endl;
    cout <"运行时间:" <1000.0 <"秒";
    return 0;
}

python用方法求区间和 python求区间素数_python用方法求区间和_08

方法2与方法1对比,方法2快了3倍以上,虽然还是超时,但已经非常接近1秒内了。

问:循环从9开始,那除了2之外,3~8会被忽略吗?

答:3~8中的偶数都不是质数。前面曾说过,在所有偶数中,只有2才是质数。所以忽略3~8是没有影响的,而且数组a初始化时默认全部标记为0,即标记为质数。

再优化(方法3)

在方法2的基础再优化。内层循环的作用是将i的倍数的合数标记为1,但如果i是合数,是没有必要进行循环,应该换下一个数。

所以,可以在内层循环之前对标记a[i]的值进行判断,如果a[i]等于标记1,说明是合数,执行continue中断本次循环,换一下个数继续判断。

#include 
#include 
using namespace std;
bool a[200000001];
int main(){
    long long n = 2e7, cnt = 1;
    int start = clock(); // 开始时间
    for (long long i = 3; i <= n; i += 2) // 遍历3~2e6之间的奇数
    {
        if (a[i]) continue; // 如果是合数,则中断,换下一个数
        for (long long j = 3; i * j <= n; j += 2) // j是当前i的倍数
            a[i * j] = 1; // 将合数标记为1
        if (!a[i]) cnt++; // 如果是质数,则计数器+1
    }
    int end = clock(); // 结束时间
    cout <"共有 " <" 个质数" <endl;
    cout <"运行时间:" <1000.0 <"秒";
    return 0;
}

python用方法求区间和 python求区间素数_筛选法_09

对于2e7的量级,可以通过,甚至比方法2快了将近3倍。但如果是2e8或2e9的量级呢?

python用方法求区间和 python求区间素数_python用方法求区间和_10

很明显超时。

以上是埃氏筛选法的基本原理和优化过程。对于2e8以上量级,将在《高级篇》中详细讲解。

因为时间问题,篇幅过长,难免会出现纰漏,请不吝指正。

END