AcWing
  • 首页
  • 活动
  • 题库
  • 竞赛
  • 校园
  • 应用
  • 文章
    • 题解
    • 分享
    • 问答
  • 吐槽
  • 登录/注册

AcWing 868. 筛质数    原题链接    简单

作者: 作者的头像   Jasper08 ,  2022-05-12 18:40:28 ,  所有人可见 ,  阅读 24


1


题目描述

给你一个数 $n$,请你求出区间 $[2,n]$ 中共有多少个质数。

样例

输入样例1:

8

输出样例1:

4

算法1 埃氏筛

基本思路

朴素的埃氏筛很简单。用区间 $[2,n]$ 中的每一个数去筛掉它的倍数即可。

bool st[N]; //st[i]记录i是不是合数

int cntprime(int n) {
    int cnt = 0;
    for (int i = 2; i <= n; ++i) {
        if (!st[i]) //st[i]不是合数
            cnt ++;
        for (int j = i+i; j <= n; j += i) //筛去i的所有倍数
            st[j] = 1;
    }
    return cnt;
}

我们如何求它的复杂度呢?

这个代码执行了 $(n/2+n/3+n/4+…+n/n)$ 次。我们有:

$$\lim_{n \to \infty} \left (1/1+1/2+1/3+…+1/n\right )=\ln n+\gamma $$

其中,$\ln n$ 表示以 $e$ 为底的自然对数,即 $\ln n=\log_e n$,$\gamma$ 是欧拉常数,约等于 $0.577$.

而 ${\small n/2+n/3+n/4+…+n/n=n(1/1+1/2+1/3+…+n/n)-n=n\ln n+\gamma\times n-n}$.

这个结果可以粗略地看为 $n\ln n$.

而 $\ln n=\log_e n<\log_2 n$,所以朴素的埃氏筛的时间复杂度是 $O(n\log n)$.

但其实朴素的埃氏筛有一点浪费了时间:它会筛去合数的倍数。例如,$2$ 已经筛掉了 $8$ 和 $12$,但 $4$ 还要再筛它们一遍,所以我们只需要筛质数的倍数即可。

给出要筛数值的范围 $n$,找出以内的素数。先用 $2$ 去筛,即把 $2$ 留下,把 $2$ 的倍数剔除掉;再用下一个质数,也就是 $3$ 筛,把 $3$ 留下,把 $3$ 的倍数剔除掉;接下去用下一个质数 $5$ 筛,把 $5$ 留下,把 $5$ 的倍数剔除掉;不断重复下去……

参照上面的方法,我们可以写出改进版的埃氏筛的算法:

bool st[N]; //st[i]记录i是不是合数

int cntprime(int n) {
    int cnt = 0;
    for (int i = 2; i <= n; ++i) {
        if (!st[i]) { //st[i]不是合数
            cnt ++;
            for (int j = i+i; j <= n; j += i) //筛去i的所有倍数
                st[j] = 1;
        }
    }
    return cnt;
}

根据素数定理,可知区间 $[1,n]$ 内大约有 $\dfrac{n}{\ln n}$ 个质数。

而 $n\ln n / \ln n=n$.

所以改进版的埃氏筛的时间复杂度大约是 $O(n)$.

但实际上,改进版的埃氏筛的时间复杂度是 $O(n\log\log n)$,但 $\log\log n$ 可以粗略地看作一个常数。例如,当 $n=2^{32}$ (unsigned int 的最大值加上 $1$ )时,$\log\log n=\log 32=5$.

完整代码

#include <iostream>
#include <cstdio>

using namespace std;

const int N = 1000010;

bool st[N];

int cntprime(int n) {
    int cnt = 0;
    for (int i = 2; i <= n; ++i) {
        if (!st[i]) {
            cnt ++;
            for (int j = i+i; j <= n; j += i)
                st[j] = 1;
        }
    }
    return cnt;
}

int main() {
    int n;
    scanf("%d", &n);
    int ans = cntprime(n);
    printf("%d", ans);
    return 0;
}

算法2 线性筛

基本思路

尽管改进版的埃氏筛已经很快了,但仍然有一个不足。例如:对于 $15$,$3$ 会筛掉它,但 $5$ 也会筛掉它。线性筛在埃氏筛的基础上进行了改进,只用一个数的最小质因子筛去它。

首先定义一个 bool 类型数组 $st$( $st_i$ 表示 $i$ 是不是合数),一个 int 类型数组 $pr$ ( $pr_i$ 表示第 $i+1$ 个质数) 和计数器 $cnt$,表示区间 $[2,n]$ 之间质数的数量。

int pr[N], cnt;
bool st[N];

然后遍历区间 $[2,n]$,如果 $st_i = 0$,说明 $i$ 是质数,将其存储到 $pr$ 数组中。

for (int i = 2; i <= n; ++i) {
    if (!st[i])
        pr[cnt ++] = i;

     //... 
}

随后,从第一个质数开始,筛去质数与 $i$ 的乘积,直到 $i$ 第一次被一个质数 $pr_j$ 整除。此时,$pr_j$ 是 $i$ 的最小质因数,跳出循环。

for (int j = 0; pr[j] <= n/i; ++j) {
    st[pr[j] * i] = 1;
    if (i % pr[j] == 0)
        break;
}

顾名思义,线性筛的复杂度是线性的,即 $O(n)$.

完整代码

#include <iostream>
#include <cstdio>

using namespace std;

const int N = 1000010;

int pr[N], cnt;
bool st[N];

void cntprime(int n) {
    for (int i = 2; i <= n; ++i) {
        if (!st[i])
            pr[cnt ++] = i;

        for (int j = 0; pr[j] <= n/i; ++j) {
            st[pr[j] * i] = 1;
            if (i % pr[j] == 0)
                break;
        }
    }
}

int main() {
    int n;
    scanf("%d", &n);
    cntprime(n);
    printf("%d", cnt);
    return 0;
}

0 评论

你确定删除吗?
1024
x

© 2018-2022 AcWing 版权所有  |  京ICP备17053197号-1
用户协议  |  常见问题  |  联系我们
AcWing
请输入登录信息
更多登录方式: 微信图标 qq图标
请输入绑定的邮箱地址
请输入注册信息