一篇文章彻底搞懂线性筛(素数筛)##C语言

本文需要求解的问题是:输入一个数字n,输出比n小的所有素数。

基础

我们不着急开始线性筛的证明,从基础开始,先来了解一下什么是素数,什么是合数。

合数和因数

假定有一个数x,它能被两个数相乘表达出来,即a*b=x,而且这个a、b都不为0,那么这个数x就是合数,与此同时a、b就是它的两个因数。

素数

假定有一个数x,它只能被1和它本身相乘所表示,那么这个数就是素数。

由此,我们知道了一个概念:自然数的集合是由素数和合数两种数组合起来的。

 质因数

既是质数也是因数的数就是质因数。

例如,5是一个质数,同时也是25的因数,那么这个数也就是25的一个质因数。

 整除

除法都会吧?

如果有a*b=c,我们就称c能够整除a或者a能够整除b。我们记作c|b或者c|a。

记住上面的基础知识,这对于你接下来对每个方法的理解很重要!

接下来我们从暴力破解到一点点的优化到素数筛的发展逐一的介绍:

暴力求解

想要判断一个数是不是素数,从他的定义入手:

假定有一个数x,它只能被1和它本身相所表示,那么这个数就是素数。

我们就看看它有没有除了1和它本身之外的因数呗,直接循环遍历比他小的所有值就可以了:

#include <stdio.h>

int main() {
  int n;
  int flag = 1;  //1:默认值,认为当前遍历到的值为素数
  scanf("%d", &n);
  for (int i = 2; i <= n; i++) {
      for (int j = 2; j < i; j++) { 
        if (i % j == 0) { //发现当前遍历到的值有其他的因数,不是素数flag = 0
          flag = 0;
          break; //并且不再循环
        }
      }
      if (flag) printf("%d\n", i); //输出素数
      flag = 1; //为下一次循环做准备,重新将flag置为1
  }
  return 0;
}

这就是最简单的暴力破解算法,对于每一个数我们都去寻找比他小的“可能存在的因数”,当然,你也可以发现:偶数一定不是素数的,1、11、111、1111一定都是素数,基于此你也可以产生很多优化:但是今天的主题并不是这个,再此就不细究。

真正的筛——埃式筛

普通的暴力破解有个特点:

我们一直在关注素数的特性,想着如何去将素数找出来,但是却忽略了合数。

假设我们将所有的合数都进行标记,那剩下的数不就都是素数了吗?

根据合数的定义我们知道:合数一定存在不为1和它本身的因数,那么假设有一个数q,那么1q,2q,3q,4q...一定是合数呀!

基于此,真正的筛,诞生了!

我们观察自然数,0和1不去考虑,他们本身就不是合数,那么2是一个素数,既然这样,我们就把2的倍数全部标记!这些数都是合数!

之后发现3是素数,那么3的所有倍数也都是合数!继续标记!

......

如何用代码来表示我们的想法呢?

我们可以开辟一块足够大的空间,也就是一个数组,这个数组各部分的语义如下:

  • 下标:表示每一个数字
  • 存储内容:表示对应下标数字的状态,也就是说是否为素数

接下来举个例子:

假如我们想要输出100之前的所有素数,我们的数组就要大于100,初始时每个存储空间存储的都为0,表示我们假定所有数都是素数(就像一个筛糠的过程,一开始糠全部在筛子上面)。

然后依次遍历每一个值,从2开始,我们发现2是素数,那么接下来我们就取循环标记数组中下标为2的倍数的值,将他们存储的空间中记为1,表示他们为合数。

遍历到3,继续去标记3的倍数,以此类推......

代码如下:

#include <stdio.h>
#include <math.h>

int book[1000000] = {0}; //数组

int main() {
    int n;
    int temp; //标记过程中遍历到的倍数的最大值
    scanf("%d", &n);
    for(int i = 2; i <= n; i++) {
        temp = n / i;  //倍数的最大值就是输入数字n除以当前素数
        if(!book[i]) {
            for (int j = 2; j <= temp; j++) {
                book[j * i] = 1;
            }
        }
    }
    for(int i = 2; i <= n; i++) {
        if (!book[i]) printf("%d\n", i); // 循环输出所有素数
        else continue;
    }
    return 0;
}

我们会发现一个问题:

当i = 2 => 2i=4,3i=6,4i=8...
当i = 3 => 2i=6,3i=9,4i=12...
当i = 4 => 2i=8...

发现了吗?我们似乎重复标记了一些数字,那该怎么解决这个问题呢?

接下来就是埃式筛的升级版,也是最终版——线性筛:

线性筛

我们来分析一下重复标记的原因:在上面的埃式筛中,当我们遇到素数后,将它的倍数全部标记,由此可以推断:一个数被重复标记的原因是因为它是不同质数的因数。

也就是说,我们使用了多个质因数去标记了这个合数。

例如:

12被2和3标记过,30被2、3、5同时标记过。

分析出了原因,优化方向就呼之欲出了:

我们只要使用最小质因数去标记这个数(十分重要,这是线性筛的核心原理所在)就行了,具体该怎么做呢?

具体来说就是维护一个标记数组 arr 和一个已有素数数组 primes ,然后,我们从2开始遍历所有数 i ,并且:(接下来这两条好好理解!!核心部分!!)

①把遇到所有未被标记为合数的数 i (埃式筛中从小到大遍历时遇到的一个个质数)存到 primes 里去;

②以 i 为第一个因子,分别以 primes 里每个质数 j 为第二个因子,求积,可以断定 j∗i 必为合数,故标记之;同时,若 mod(i,j)==0 ,说明 j 是 i∗j 的一个质因数,又因为 primes 数组中的元素是递增的, j 是第一个可以除断 i 的,故可断定 j 是 i 的最小质因数,同时也是 i∗j 的最小质因数,那么更进一步也可断定j 之后的质数 j′ 一定不是 i∗j′ 的最小质因数,故结束 primes 的遍历。

上代码:

#include <stdio.h>
#include <math.h>

int    arr[1000000] = {0};  //对应上面的埃式筛的book数组
int primes[1000000] = {0};  //用于存储目前发现的质数的数组
int main() {
    int n, pos = 0; //n的语义没变,pos表示primes数组中存放个数,同时对应其下标,从1开始
    scanf("%d", &n);
    for (int i = 2; i <= n; i++) {
        if(!arr[i]) primes[++pos] = i; //同埃式筛,如果未被标记,那么这个数为素数,放入primes数组
        for (int j = 1; j <= pos; j++) {  //这一部分是标记部分,对应我文章中写的两条来理解!!
            if (i * primes[j] > n) break;  //超出范围,停止循环
            arr[i * primes[j]] = 1; // 记住,我们始终只让最小的质因数作为因数去标记后面的数字!!
            if (i % primes[j] == 0) break; //灵魂语句,当发现i能被该质数整除时,那么这个primes[i]就是最小质因数,我们让最小质因数去标记的目的达到了,结束循环!
        }
    }
    for (int i = 2; i <= n; i++) {
        if (!arr[i]) printf("%d\n", i);
    }
    return 0;
}

这个图片说明了线性筛的执行过程(从左到右从上到下):

红色是被优化的部分。

为了帮助理解,我们根据代码过程和前几个数进行一下过程模拟:

我们分块进行:

首先,所有的数都默认为素数,i 从2开始遍历,

2为素数,丢到primers数组中。

接下来开始标记环节:当前i为2,数组中只有2

因此只遍历一个值:2 * 2 = 4,此时4被标记;

此时2 % 2 = 0,停止循环,说明4的最小质因数是2

接下来,i 遍历到3,

3也是素数,丢到primers数组中。

标记环节:i 为3,数组中有2、3两个值

2 * 3 = 6, 3 * 3 = 9,此时6和9被标记

此时3 % 3 = 0,停止循环,说明9的最小质因数是3

接下来,i 遍历到4,

4不是素数,跳过

接下来,i 遍历到5,

5也是素数,丢到primers数组中。

标记环节:i 为5,数组中有2、3、5两个值

2 * 5 = 10, 3 * 5 = 15,5 * 5 = 25此时10和15和25被标记

此时5 % 5 = 0,停止循环,说明25的最小质因数是5

 ......

线性筛的改进实际上就是“每个合数只被它的最小质因数筛去”,但实现的时候需要以下两个逻辑支撑:

一、每个合数只筛一次

①算法特性决定了找到的素数只能从自身开始乘。比如找到素数5时,它的2、3、4倍已经删不到了,这样避免了重复删。(虽然单独列出但也可以合并进②)

②if(i%primers[j]==0)break;语句实现不重复删。证明很简单,自然数 i 从最小素数2开始乘,         i×primers[j] 以每个 primers[j] 为最小质因数,直到 primers[j] 整除 i 时,i×primers[j] 还是以primers[j]为最小质因数,但下一个数 i×primers[j+1]=(i/primers[j])×primers[j]×primers[j+1]中primers[j+1]就不是最小质因数了,所以不必继续乘了。

二、不能干扰到运用埃氏筛核心原理找素数

①与②共同实现不重复筛的同时导致了素数倍数的缺失,比如程序运行到s7时,7是个标记素数(没被当合数删除),那么素数2、3、5要从2倍一直删到倍数大于7才能保证7确实是个素数,但明眼的看表就知道缺失了好多素数倍数啊,怎么能证明这些缺失不影响素数的确定呢?

证明:首先①②中不重复筛的合数最终会出现在图表中(延迟删去)。设L是标记素数,把小于L的所有素数倍数放入集合Q中,用q表示Q中任意素数倍数,则q<L<2L,由于图中素数倍数的大小排列特性,q<2L意味着小于L的素数倍数都会在步骤s(L)之前被删去,这样得证①和②不影响素数的确定。因L不被小于它的所有素数整除,所以L是素数。

综上就是线性筛的算法逻辑,为证明它需要考虑的出乎意料地多,理解“if(i%primers[j]==0)break;”语句也才仅仅是理解皮毛(值得一提的是,即使删去此语句,余下的部分也是相当优秀的程序)。

 

 

线性筛部分参考博客:

一图看懂线性筛原理 - 知乎 (zhihu.com)       

关于从埃氏筛到线性筛你不会想知道的那些事(证明,慎入) - 知乎 (zhihu.com)

  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

若亦_Royi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值