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(n∗lnlnn) 效率已然很高了,但这样会重复筛去,2*3=6
和 3*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) 内判断一个数是否是素数,但存在一定的误差。其中包含有一部分基础数论、概率相关知识,就不在这里展开详解了,点到此地,有兴趣可自行了解。