学习笔记:素数筛

素数筛

引入(不正经版)

yzj:这些是都要讲的,老曹尤其点明了筛法

ty:老曹还点明了组合数学……

hhy:我筛 nm!cnm!!我 tm,我 tm 把你 m 筛了 wc!!!

(需要原录音可以找这个天天搞颓玩手机从不学习的 $\to$ tsq

实现

素数是什么想必大家都知道(然而我不知道……)。

暴力做法(试除法)

由素数的定义可知,素数是只有 $1$ 和它本身两个约数的数。那么,根据定义我们可以很快地敲出如下代码:

bool f(int x){
    if(x < 2)return false;
    for(int i = 2  ; i <= sqrt(x) ; i ++)
        if(x % i == 0)return false;
    return true;
}

这里有一些常数优化:

  1. 显然 $1$ 一定是 $x$ 的约数,故从 $2$ 开始判断。
  2. 只需循环到 $\sqrt{n}$。
  3. $0$ 和 $1$ 显然既不是素数也不是合数。

证明:

性质 $1$ 和性质 $3$ 显然成立,我们仅证性质 $2$。

对于性质 $2$,我们采用反证法。

假设性质 $2$ 不成立,那么这样的数 $x'$ 一定满足 $\sqrt{x}\le x'\le x-1$。

因为 $x'$ 能整除 $x$,所以它们的商 $x/x'$ 也能整除 $x$。

然而 $2\le x/x'\le \sqrt{x}$。

这与假设产生矛盾。

故该假设不成立,原命题成立。

证毕。

这种方法被称作试除法,它的原理显然是依据素数定义暴力判断每一个可能的约数,这种方法的时间复杂度为 $O(\sqrt{n})$。

对于某一个素数的判断,这种方法是比较实用的。然而在某些情况下我们可能需要预处理出大量素数,这种时候试除法就显得有些力不从心了,我们需要寻找一种更加优秀的做法。

Eratosthenes 筛法

Eratosthenes 筛法基于这样的想法:任意整数 $x$ 的倍数 $2x$,$3x$,…都不是素数。根据素数的定义可知,上述命题显然成立。

我们可以从 $2$ 开始,由小到大扫描每个数 $x$,把它的倍数 $2x$,$3x$,…,$\lfloor n/x\rfloor\times x$ 标记为合数。当扫描到一个数时,若它尚未被标记,则它不能被2~x-1之间的任何数整除,该数就是素数。

另外,我们可以发现,$2$ 和 $3$ 都会把 $6$ 标记为合数。实际上,小于 $x^2$ 的x的倍数在扫描更小的数时就已经被标记过了。因此,我们可以对 Eratosthenes 筛法进行一定的常数优化,对于每个数 $x$,我们只需要从 $x^2$ 开始,把 $x^2$,$(x+1)\times x$,…,$\lfloor n/x\rfloor\times x$ 标记为合数即可。

可以根据下图感性理解一下:

Eratosthenes 筛法的时间复杂度为 $O(\sum_{prime\le n}\frac{n}{p})=O(n\log\log n)$。该算法实现简单,效率已经非常接近于线性,是算法竞赛中最常用的质数筛法。

for(int i = 2 ; i <= n ; i ++)
    if(isp[i] == false){
        pri[++cnt] = i;
        for(int j = i ; i * j <= n ; j ++)
            isp[i * j] = true;
    }

线性筛法(欧拉筛)

即使在优化后(从 $x^2$ 开始),Eratosthenes 筛法仍然会重复标记合数。例如 $12$ 既会被 $2$ 又会被 $3$ 标记,在标记 $2$ 的倍数时,$12=6\times 2$,在标记 $3$ 的倍数时,$12=4\times 3$。其根本原因是我们没有确定出唯一的产生 $12$ 的方式。

线性筛法通过“从大到小累积质因子”的方式标记每个合数,即让 $12$ 只有 $3\times 2\times 2$ 一种产生方式。设数组 $pri$ 记录每个数的最小质因子,我们按照以下步骤维护$pri$。

  1. 依次考虑区间 $[2,n]$ 中的每一个数 $i$。

  2. pri[i] = i,说明 $i$ 是素数,把它保存下来。

  3. 扫描不大于 $pri[j]$ 的每个素数 $p$,令 pri[i * p] = p。也就是在 $i$ 的基础上累积一个质因子 $p$。因为 p <= pri[j],所以 $p$ 就是合数 $i\times p$ 的最小质因子。

每个合数 $i\times p$ 只会被它的最小质因子 $p$ 筛一次,时间复杂度为 $O(n)$。

    for(int i = 2 ; i <= n ; i ++){
        if(b[i] == false){
            tmp++;a[tmp] = i;
        }
        for(int j = 1 ; j <= tmp && i * a[j] <= n ; j ++){
            b[i * a[j]] = true;
            if(i % a[j] == 0)break;
        }
    }

【模板】线性筛素数

题目背景

本题已更新,从判断素数改为了查询第 $k$ 小的素数
提示:如果你使用 cin 来读入,建议使用 std::ios::sync_with_stdio(0) 来加速。

题目描述

如题,给定一个范围 $n$,有 $q$ 个询问,每次输出第 $k$ 小的素数。

输入格式

第一行包含两个正整数 $n,q$,分别表示查询的范围和查询的个数。

接下来 $q$ 行每行一个正整数 $k$,表示查询第 $k$ 小的素数。

输出格式

输出 $q$ 行,每行一个正整数表示答案。

样例 #1

样例输入 #1

100 5
1
2
3
4
5

样例输出 #1

2
3
5
7
11

提示

【数据范围】
对于 $100\%$ 的数据,$n = 10^8$,$1 \le q \le 10^6$,保证查询的素数不大于 $n$。

Data by NaCly_Fish.

#include <iostream>
#define MAXN 300000005
using namespace std;
int n, q, tmp;
int a[MAXN];
bool b[MAXN];
int read(){
    int t = 1, x = 0;char ch = getchar();
    while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();}
    while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
    return x * t;
}
void write(int x){
    if(x < 0){putchar(x % 10 + '0');x = -x;}
    if(x >= 10)write(x / 10);
    putchar(x % 10 + '0');
}
int main(){
    n = read();q = read();
    for(int i = 2 ; i <= n ; i ++){
        if(b[i] == false){
            tmp++;a[tmp] = i;
        }
        for(int j = 1 ; j <= tmp && i * a[j] <= n ; j ++){
            b[i * a[j]] = true;
            if(i % a[j] == 0)break;
        }
    }
    for(int i = 1 ; i <= q ; i ++){
        tmp = read();write(a[tmp]);putchar('\n');
    }
    return 0;
}
posted @ 2023-09-26 20:43  tsqtsqtsq  阅读(30)  评论(0)    收藏  举报  来源