洛谷题单指南-数学基础问题-P3383 【模板】线性筛素数

原题链接:https://www.luogu.com.cn/problem/P3383

题意解读:素数筛模版题。

解题思路:

素数筛介绍

所谓素数(质数),是指除了1和它本身以外不再有其他因数的自然数,一般用试除法判断素数(时间复杂度:O(sqrt(n))):

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

注意:如果i比较大,i * i <= x有溢出风险,写成i <= x / i比较安全。

所谓素数筛,就是在一定范围内,筛出所有的素数。

例如要筛出1~n中所有的素数,如果枚举每一个数去判断是否是素数,时间复杂度为O(n*sqrt(n)),当n=106都无法应对,因此出现了素数筛算法。

素数筛算法的核心思想是通过遍历2~n,将每个数的若干倍数都标记为合数,这样剩下的就都是素数,根据优化程度不同有三种算法:

a、朴素筛法:时间复杂度O(n*logn)

对于2~n中每一个数,如果没有标记成合数,则保存为素数,再将该数的2倍、3倍、4倍...标记为合数,代码如下:

#include <bits/stdc++.h>
using namespace std;

const int N = 1e7;

int primes[N], cnt = 1; //保存筛出的素数
bool flag[N]; //标记合数

//朴素筛,筛出1 ~ N之间的素数,复杂度O(nlogn)
void getprimes1()
{
    for(int i = 2; i <= N; i++)
    {
        if(!flag[i]) primes[cnt++] = i; //如果i未被标记合数,则保存i为素数
        for(int j = i + i; j <= N; j += i) //把i的倍数都标记为合数
            flag[j] = true;
    }
}

int main()
{
    getprimes1(); 
    return 0;
}

b、埃氏筛法:时间复杂度O(n*loglogn)

由于在标记合数时,无论当前i是素数还是合数,都将其倍数进行了标记,这样必然导致大量重复标记。

例如:i=2时,i*4=8;而i=4时,又有i*2=8;8被重复标记

埃氏筛就是在朴素筛的基础上做出优化,只将素数的倍数进行标记,合数的倍数不标记,这样可以一定程度减少重复标记,代码如下:

#include <bits/stdc++.h>
using namespace std;

const int N = 1e7;

int primes[N], cnt = 1; //保存筛出的素数
bool flag[N]; //标记合数

//埃氏筛,筛出1 ~ N之间的素数,复杂度O(nloglogn)
void getprimes2()
{
    for(int i = 2; i <= N; i++)
    {
        if(!flag[i]) //如果没有被标记为合数
        {
            primes[cnt++] = i; //保存素数
            for(int j = i + i; j <= N; j += i) //只对素数的倍数进行标记
                flag[j] = true;
        }
    }
}

int main()
{
    getprimes2(); 
    return 0;
}

c、线性筛法:时间复杂度O(n)

尽管埃氏筛只针对素数的倍数进行标记,但是还是会有一定程度的重复标记。

例如:i=2时,2*3=6;i=3时,i*2=6;6被重复标记。

线性筛在埃氏筛的基础上又进一步优化,使得每个合数只被它最小的素因子标记,这样每个合数都只被标记一次,如何做到呢?

#include <bits/stdc++.h>
using namespace std;

const int N = 1e7;

int primes[N], cnt = 1; //保存筛出的素数
bool flag[N]; //标记合数

//线性筛,筛出1 ~ N之间的素数,复杂度O(n)
void getprimes3()
{
    for(int i = 2; i <= N; i++)
    {
        if(!flag[i]) primes[cnt++] = i; //如果i未标记为合数,则保存为素数
        for(int j = 1; j <= cnt; j++) //从小到大遍历每一个已保存的素数,要将i * primes[j]标记为合数
        {
            if(i * primes[j] > N) break; //超出最大值
            flag[i * primes[j]] = true; //将i * primes[j]标记为合数,primes[j]必然是i * primes[j]的最小素因子
            if(i % primes[j] == 0) break; //当i中包含一个primes[j]因子,就不能用此i去标记下一个数i * primes[j+1],因为i * primes[j+1]包含因子primes[j],而primes[j+1]不是最小素因子
        }
    }
}

int main()
{
    getprimes3(); 
    return 0;
}

本题数据量在10^8,直接用线性筛求解。

100分代码:

#include <bits/stdc++.h>
using namespace std;

const int N = 1e8 + 5;

int primes[N], cnt;
bool flag[N];
int n, q, k;

int main()
{
    scanf("%d%d", &n, &q);

    for(int i = 2; i <= n; i++)
    {
        if(!flag[i])  primes[++cnt] = i;
        for(int j = 1; j <= cnt; j++)
        {
            if(i * primes[j] > n) break;
            flag[i * primes[j]] = true;
            if(i % primes[j] == 0) break;
        }
    }

    while(q--)
    {
        scanf("%d", &k);
        printf("%d\n", primes[k]);
    }
    
    return 0;
}

 

posted @ 2024-04-10 11:11  五月江城  阅读(92)  评论(0编辑  收藏  举报