[数论基础] 1. 输出素数(素数筛、线性筛、Miller-Rabbin 素性测试、巧妙解法)

1. 题目来源

链接:79. 输出素数

2. 题目说明

在这里插入图片描述

3. 题目解析

方法一:素数筛+巧妙解法

题意很明确,这应该是学习完语言基础,甚至是循环判断基础后就应该会做的题目。但是会做不代表能在这道题目上拿到满分。

光是这一道题目就能产生 5 个层次的学生,看看你在哪一层呢?

第一层:0 分,不会写,过不了,没思路

第二层:40 分,最暴力的方式解决,但忘记考虑边界 0 1情况,仍采用 cin、cout,但数据不强,在此不受影响

参见代码如下:

#include <iostream>
#include <cmath>

using namespace std;

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

int main() {
	int a, b;
	cin >> a >> b;
	for (int i = a; i <= b; ++i) {
		if (is_prime(i)) cout << i << endl;
	}
	return 0;
}

第三层:50分,算是及格分了,最暴力的方式解决,考虑边界 0 1情况,仍采用 cin、cout,但数据不强,在此不受影响

参见代码如下:

#include <iostream>
#include <cmath>

using namespace std;

bool is_prime(int x) {
	// 加一行,多10分
	if (x == 0 or x == 1) return false;
	for (int i = 2; i < x; ++i) {
		if (x % i == 0) return false;
	}
	return true;
}

int main() {
	int a, b;
	cin >> a >> b;
	for (int i = a; i <= b; ++i) {
		if (is_prime(i)) cout << i << endl;
	}
	return 0;
}

第四层:70分,算是及格分了,开根号优化,考虑边界 0 1情况,仍采用 cin、cout,但数据不强,在此不受影响,在此即便换成 scanf、printf 也仅有 70 分

参见代码如下:

#include <iostream>
#include <cmath>

using namespace std;

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

int main() {
	int a, b;
	cin >> a >> b;
	for (int i = a; i <= b; ++i) {
		if (is_prime(i)) cout << i << endl;
	}
	return 0;
}

第五层:100分,完美的解答,采用素数筛,优化效率

这个也就是本篇博文的意义所在了,素数筛 算法,思想很简单,就考你知道还是不知道。在此首先来看看 一般筛,也就是 埃氏筛。下面依据紫书的 10.1.2节有关讲解进行说明。筛法的思想特别简单:对于不超过 n 的每个非负整数 p,删除 2p, 3p, 4p,… 当处理完所
有数之后,还没有被删除的就是素数。如果用 vis[i] 表示i已经被删除,筛法的代码可以写成:

memset(vis, 0, sizeof(vis));
for(int i = 2; i <= n; ++i)
  for(int j = i*2; j <= n; j += i) vis[j] = 1;

循环总次数会小于 O ( n l o g n ) O(nlogn) O(nlogn),时间复杂度为 O ( n ∗ l n l n n ) O(n*lnlnn) O(nlnlnn) 效率已然很高了,但这样会重复筛去,2*3=63*2=6会重复筛,但即便这样,效率仍接近线性。有兴趣可自由证明。

在此挂上大佬链接:普通筛法时间界的证明

下面来改进这份代码。首先,在 对于不超过 n 的每个非负整数 p 中,p 可以限定为素数,只需在第二重循环前加一个判断 if(!vis[i]) 即可。另外,内层循环也不必从 i*2 开始,它已经在 i=2 时被筛掉了。改进后的代码如下:

memset(vis, 0, sizeof(vis));
for(int i = 2; i * i <= n; ++i) if(!vis[i])
  for(int j = i*i; j <= n; j += i) vis[j] = 1;

完成版如下,但仍只能获取 70 分,但是这并不是算法的问题,而是 cin、cout 太拉胯了,得改成 scanf、printf,就好了。

参见代码如下:

#include <iostream>
#include <cmath>

using namespace std;

const int MAXN = 1e7 + 50;
int prime[MAXN];

void init_prime() {
	prime[0] = prime[1] = 1;
	for (int i = 2; i * i <= MAXN; ++i) {
		if (prime[i]) continue;
		for (int j = 2 * i; j <= MAXN; j += i) {
			prime[j] = 1;
		}
	}
	return;
}

int main() {
	init_prime();
	int a, b;
	cin >> a >> b;
	for (int i = a; i <= b; ++i) {
		if (prime[i] == 0) cout << i << endl;
	}
	return 0;
}

cin、cout,改成 scanf、printf,终于获取满分。

参见代码如下:

#include <iostream>
#include <cmath>

using namespace std;

const int MAXN = 1e7 + 50;
int prime[MAXN];

void init_prime() {
	prime[0] = prime[1] = 1;
	for (int i = 2; i * i <= MAXN; ++i) {
		if (prime[i]) continue;
		for (int j = 2 * i; j <= MAXN; j += i) {
			prime[j] = 1;
		}
	}
	return;
}

int main() {
	init_prime();
	int a, b;
	scanf("%d%d", &a, &b);
	for (int i = a; i <= b; ++i) {
		if (prime[i] == 0) printf("%d\n", i);
	}
	return 0;
}

方法二:线性筛+巧妙解法

线性筛即 欧拉筛,将素数判断的时间复杂度优化至 O ( n ) O(n) O(n),其思想仍继承自上述的埃氏筛,埃氏筛的效率已经很高了,但其不足之处在于仍存在很多数被重复判断,做了这些不必要的操作必然影响效率。欧拉筛针对这点进行改善。

参见代码如下:

#include <iostream>
#include <string.h>
#include <cmath>

using namespace std;

const int MAXN = 1e7 + 50;
int prime[MAXN], c = 0;
bool number[MAXN];

void init_prime(int n) {
    for (int i = 2; i <= n; i++) {
        if (!number[i]) prime[c++] = i;
        for (int j = 0; j < c and prime[j] * i <= MAXN; j++) {
            number[prime[j] * i] = true;
            //保证每个合数只会被它的最小质因数筛去,因此每个数只会被标记一次
            if (i % prime[j] == 0)
                break;
        }
    }
}

int main() {
	int a, b;
	scanf("%d%d", &a, &b);
    init_prime(b);
	for (int i = 0; i < c; ++i) {
        if (prime[i] < a) continue;
		else printf("%d\n", prime[i]);
	}
	return 0;
}

prime 数组存放小于 n 的所有素数,其个数为 c 个。prime 数组中的素数是递增的,当 i 能整除 prime[j],那么 i * prime[j + 1] 这个合数肯定被 prime[j] 乘以某个数筛掉。因为 i 中含有 prime[j]prime[j]prime[j+1] 小,即 i=k*prime[j],那么 i*prime[j+1]=(k*prime[j])*prime [j+1]=k’*prime[j],接下去的素数同理。所以不用筛下去了。因此,在满足 i%prime[j]==0 这个条件之前以及第一次满足改条件时,prime[j] 必定是 prime[j]*i 的最小因子。故不会对一个元素进行重复判断,将效率提升到 O ( n ) O(n) O(n)

方法三:Miller-Rabbin 素数测试+巧妙解法

这个方法是在前段时间做一个关于 RSA 加密项目了解到的,适合大数的素性测试,500 位以上的大数速度也是很快的,在 boost 库中自带有大数及 Miller-Rabbin 素数测试函数接口,配合起来使用实在是太方便了。它基于随机算法,可以在 O ( l o g n ) O(logn) O(logn) 内判断一个数是否是素数,但存在一定的误差。其中包含有一部分基础数论、概率相关知识,就不在这里展开详解了,点到此地,有兴趣可自行了解。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
费马素性检验算法、Solovay-Stassen素性检验算法和Miller-Rabin素性检验算法都是用于判断一个数是否为素数的算法,下面是它们的区别和联系: 1.费马素性检验算法 费马素性检验算法是一种基于费马小定理的素性测试算法。它的原理是:如果p是一个素数,a是小于p的正整数,则a^(p-1) mod p = 1;如果p不是素数,那么对于任意小于p的正整数a,a^(p-1) mod p != 1。因此,我们可以在随机选择的a值下,使用快速幂算法来计算a^(p-1) mod p的值,如果结果不等于1,则p一定不是素数。 缺点:费马素性检验算法存在漏报的情况,即有时候会将合数误判为素数。 2.Solovay-Stassen素性检验算法 Solovay-Stassen素性检验算法是一种基于欧拉准则的素性测试算法。它的原理是:如果p是一个素数,a是小于p的正整数,则a^((p-1)/2) mod p = +-1;如果p不是素数,那么对于任意小于p的正整数a,a^((p-1)/2) mod p != +-1。因此,我们可以在随机选择的a值下,使用快速幂算法来计算a^((p-1)/2) mod p的值,如果结果不等于+-1,则p一定不是素数。 缺点:Solovay-Stassen素性检验算法比费马素性检验算法更加复杂,但依然存在漏报的情况。 3.Miller-Rabin素性检验算法 Miller-Rabin素性检验算法是一种基于费马小定理的素性测试算法,它是目前最常用的素性检验算法之一。它的原理是:如果p是一个素数,a是小于p的正整数,则a^(d*2^r) mod p = 1或者p-1,其中d是一个奇数,2^r是p-1的一个因子;如果p不是素数,那么对于任意小于p的正整数a,a^(d*2^r) mod p != 1或者p-1。因此,我们可以在随机选择的a值下,使用快速幂算法来计算a^(d*2^r) mod p的值,如果结果不等于1且不等于p-1,则p一定不是素数。为了提高精度,Miller-Rabin算法通常会多次进行检验。 优点:Miller-Rabin素性检验算法的误判率很低,可以满足绝大部分应用需求。同时,Miller-Rabin算法的时间复杂度比Solovay-Stassen算法更低。 联系:这三种算法都是基于数论定理进行素性检验的,但是原理和具体实现方法有所不同。费马素性检验算法和Solovay-Stassen素性检验算法都有漏报的情况,而Miller-Rabin素性检验算法的误判率较低。因此在实际应用中,Miller-Rabin算法更加常用。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Ypuyu

如果帮助到你,可以请作者喝水~

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

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

打赏作者

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

抵扣说明:

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

余额充值