[算法练习笔记] 5、筛质数 + 前缀和解决 非素数个数问题

一、筛质数算法

名词解释:
质数: 只有1和它本身两个因数(约数),那么这样的数叫做质数,也叫素数
合数: 除了能被1和它本身整除,还能被其他的正整数整除,那么这样的数叫做合数
筛质数: 将筛选范围内的质数全部筛选出来
待查区间:1 ~ n

筛选方法如下:

1、最简单筛质数法

也就是枚举从2 ~ n的所有数,一个个来判断是否为质数。
时间复杂度约为O(n²)最劣

关键代码

for(int i=1;i<=n;++i)     //枚举1到n
{
    bool flg=0;
    for(int j=2;j<i;++j)  //枚举2到i
    {
        if(i%j==0)        //如果i%j=0,也就是i已经不为素数了
        {
            flg=1;        //打上标记
            break;        //跳出循环,不用再枚举了
        }
    }
    if(flg==0)            //如果没有被打上标记
    prime[i]=1;           //prime来标记这个数是否为素数。
}

2、最简单筛质数法的优化

研究知道,在判断质数的时候,不一定需要枚举到 i − 1,只需要枚举到 √i就可以判断出来了。
因为约数是成对出现的,比如12,你找到一个约数是3,那么另一个约数就是4;其次,这两个约数一定一个小于√n一个大于√n,因为都在√n之前的话,相乘一定会小于n(因为√n * √n = n),而两个约数都在√n后,则相乘一定会大于n
所以,如果你在√之前找不到约数,那么√n之后一定没有,此时这个数就一定是质数。
时间复杂度约为O(n * √n)

关键代码

for(int i=1;i<=n;++i)        //枚举1到n
{
    bool flg=0;
    for(int j=2;j*j<=i;++j)  //枚举2到i
    {
        if(i%j==0)           //如果i%j=0,也就是i已经不为素数了
        {
            flg=1;           //打上标记
            break;           //跳出循环,不用再枚举了
        }
    }
    if(flg==0)              //如果没有被打上标记
    prime[i]=1;             //prime来标记这个数是否为素数。
}

3、暴力筛质数法

i从2开始枚举,一直到n,将i的倍数标记,标记后即为合数
时间复杂度:约为O(nlogn)

int primes[N], cnt; // primes[]是用来存储所有素数
                    // cnt == count 总数的意思,用来当计数器
                    // 全局变量和数组默认初始化为0
bool st[N]; // st[x]存储x是否被筛掉,0表示是质数,1表示是合数被筛掉了

// 最简单筛质数
void get_primes(int n)  
{
    for (int i = 2; i <= n; i ++ ) // 从2开始筛,看是不是质数
    {
        if (!st[i]) // 即st[i]是0,是质数
        {
            primes[cnt ++] = i; // 将素数数组对应位置置为i
        }
         // 再将每一个数的倍数筛掉
        for (int j = i + i; j <= n; j += i) st[j] = true;
    }
}

4、朴素筛质数法(埃氏筛法)

与最简单筛法的区别:筛掉数的倍数时,只筛掉质数的倍数,不再筛合数的倍数
此时的时间复杂度约在O(nlog(logn)) 也就是约等于O(n)
首先,定义 st[i]数组表示 i 是否为质数primes[i]储存已经找到的所有质数cnt储存当前一共找到了多少质数

int primes[N], cnt; // primes[]是用来存储所有素数
                    // cnt == count 总数的意思,用来当计数器
                    // 全局变量和数组默认初始化为0
bool st[N];         // st[x]存储x是否被筛掉,0表示是质数,1表示是合数被筛掉了

// 埃氏筛质数
void get_primes(int n)  
{
    for (int i = 2; i <= n; i ++ ) // 从2开始筛,看是不是质数
    {
        if (!st[i])               // 即st[i]是0,是质数
        {
            primes[cnt ++] = i;   // 将质数数组对应位置置为i
            // 再将每一个质数的倍数筛掉
            // (此步不筛掉合数的倍数,是因为合数可以被分解质因数,所以在前面已经被筛掉了)
        	for (int j = i + i; j <= n; j += i) st[j] = true;
        }
        // 如果for在这,则时间复杂度约为n(nlogn)。
    }
}

5、线性筛质数法

我们发现,埃氏筛已经很快了,但是还是有所不足。
因为在埃氏筛中,有很多数有可能被筛到很多次(例如 6 , 他就被 2 和 3 分别筛了一次)。所以在欧拉筛中,
我们就是在这个问题上面做了优化,使得所有合数只被筛了一次
如果当前已经枚举到了i。如果 st[i]=1 ,也就是i为质数。则 primes [cnt+1]=i,将这个质数i存到primes数组中
然后我们每一次枚举都要做这个循环: 枚举 j从 1到 cnt。stprimesj×i= 0(因为 primesj 为质数,i就表示这个数的多少倍
要把他筛掉)。

注意,接下来是重点:

如果 i mod primes[j]=0,跳出第二层循环,为什么是i % primes[j] == 0时呢?
当i % primes[j] = 0的时候如果不中止,那么将进入下一次循环,下一次循环要筛掉的数字是primes[j + 1] * i
对于primes[j + 1] * i来说,i并没有改变,那么i此时仍然满足上一步所完成的i % primes[j] = 0,即primes[j]当前
仍然是i的最小质因数,能整除i。 则primes[j + 1] * i这个数一定是primes[j]这个质数的倍数在primes[j] * i时
已经被找出并筛掉了
。所以,此时的primes[j + 1] * i再次进行判定然后被筛掉这个过程显然是重复的,就增加了时间
开销,所以当i % primes[j] = 0时,循环就可以退出了,此时已经筛出了质数primes[j]的所有倍数。
由于所有的合数从小到大只被筛了一次,这个过程是线性的,所以这种方法叫做线性筛质数法,时间复杂度约为O(n)。

int primes[N], cnt;                 // primes[]存储所有素数  注:prime n.质数,素数
bool st[N];                         // st[x]存储x是否是素数,初始全部为false
st[1]=0;                            // 令i = 2时,数组值为1,表示是素数

void get_primes(int n)
{
    for (int i = 2; i <= n; i ++ )        // 主要目的是为了筛出2 ~ n的所有质数
    {
        if (!st[i]) primes[cnt ++ ] = i;  // 若st[i]没有被筛掉,说明是质数
        for (int j = 0; primes[j] * i <= n; j ++ )  // 枚举从0开始,最大到cnt所有数 
        {
            st[primes[j] * i] = 1;         // 表示primes[j]的所有倍数都是合数 
            if (i % primes[j] == 0) break; // 已经筛出了primes[j]的所有倍数
        }
    }
}

若 n < 10⁶ 时,线性筛和埃氏筛的时间效率差不多; 若 n > 10⁷ 时,线性筛会比埃氏筛快了大概一倍。

二、前缀和算法

前缀和算法,顾名思义,求的是某个数组的前m ~ n项之和,可以用数学中数列的前n项和来理解。
以求数组S[i]至S[j](j > i)的前缀和为例,运算过程即:
S[i]至S[j](j > i)的前缀和 = S[0] ~ S[j]之和 减掉 S[0] ~ S[i]之和
注意需要定义S[0] = 0目的是统一前缀和的求法,使得无论是求S[i]至S[j](j > i)的前缀和,还是仅求S[j]的前缀和(=S[j] – S[0]),运算过程都是S[j] – S[?],重在理解这种统一处理的思想

核心代码

	for (int i = 1; i <= n; i ++ )
	{
         s[i] = s[i - 1] + a[i]; // 前i项和 = 前i - 1项和 + 第i项值
    }

三、筛质数 + 前缀和解决问题

题目描述

求 [a,b]之间的合数个数。

注意,1既不是质数,也不是合数。

输入格式
输入包含多组测试数据。
每组数据占一行,包含两个整数 a,b。

输出格式
每组数据输出一行答案,表示合数的个数。

数据范围
1 ≤ a ≤ b ≤ 10⁷,输入最多包含 10组数据。

输入样例:
1 10
1 100
输出样例:
5
74

解题思路

本题原理上来说是最简单的找素数问题,只是如果用暴力枚举方法筛选的话时间复杂度会在O(n²)量级,而本题的运算规模
和时间要求时O(n)量级,所以在执行主函数前,需要先用线性筛质数法(O(n))预处理找到所有的合数,再用前缀和思想找到从a到b之间的所有合数即非素数即可。

解题代码

#include <bits/stdc++.h>
using namespace std;
const int N = 1e7 + 10;      // 题目要求10的7次方,多开10个位置防止数组越界
int primes[N], cnt, s[N];    
bool st[N];                  // 线性筛法所需条件

void get_primes(int n)  
{
    // 线性筛质数预处理
    for (int i = 2; i <= n; i ++ )
    {
        if (!st[i]) primes[cnt ++] = i;
        for (int j = 0; primes[j] * i <= n; j ++ ) 
        {
            st[primes[j] * i] = true;
            if (i % primes[j] == 0) break;
        }
    }
    
    // 前缀和预处理
    for (int i = 2; i <= n; i ++ )
    {
        s[i] = s[i - 1];           // 统计前i项的合数的数量
        if (st[i]) s[i] ++;        // 如果st[i] = 1表示是合数,s[i]的值就 + 1
    }
}

int main() {
    int a, b;
    get_primes(N - 1);             // 预处理
    while (cin >> a >> b)          // 循环输入数据,这种写法值得借鉴
    {
        cout << s[b] - s[a - 1] << endl;
    } 
}

参考文献

1、AcWing 969 筛质数:https://www.acwing.com/activity/content/code/content/5374176/
2、洛谷:https://www.luogu.com.cn/blog/Bqianyi/zhi-shuo-shai
3、基础算法 前缀和:https://zhuanlan.zhihu.com/p/117569086
4、题目来源:AcWing 3701. 非素数个数 https://www.acwing.com/problem/content/3704/
5、https://blog.csdn.net/lx1056212225/article/details/125009857

版权声明:本文为博主作者:Timmmmmmmmmmm原创文章,版权归属原作者,如果侵权,请联系我们删除!

原文链接:https://blog.csdn.net/weixin_46019348/article/details/135899974

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
心中带点小风骚的头像心中带点小风骚普通用户
上一篇 2024年2月19日
下一篇 2024年2月19日

相关推荐