C++实现暴力筛、朴素素数筛、埃氏素数筛、欧拉素数筛的解法
前言:今天有身边的人在群里吐槽了一道烟台大学OJ上面的水题
然后他提出的问题是:如何开一个1000w大小的数组来存储。
What?我仔细看了一眼题目,觉得问题的关键并不是数组可以开多大。而是这是一道在我看来很适合用筛法的题目(具体是否真的如此还需要进行方法的比对),但是最优秀的筛法是什么这个问题在此之前还真就属于我的知识盲区。于是我百度了不少博客和资料,最后在这里总结我学到的东西。如有不对的地方烦请各位路过的大佬指正,非常感谢!!
另外,想直接看这道题我最后如何实现的小伙伴可以直接跳过中间的说明部分,直接传送到页面的底部
以上。
引述1:暴力筛
暴力筛,是每一个接触过编程求素数问题的人都必然学习过的一种算法,它从素数的定义出发(在一个大于1的自然数中,除了1和此整数自身外,没法被其他自然数整除的数),直观好理解:
#include <iostream>
using namespace std;
const int maxn = 100000;
int num = 0 , a[maxn];
void is_prime(int n)
{
for(int i = 2; i < n; i++)
{
int ok = 1;
for(int j = 2; j < i; j++)
{
if(i % j == 0)
{
ok = 0;
break;
}
}
if(ok)
{
a[num++] = i;
}
}
}
int main(void)
{
int n;
cin >> n;
is_prime(n);
for(int i = 0; i < num; i++)
{
if(i < num - 1)
{
cout << a[i] << " ";
}
else
{
cout << a[i];
}
}
return 0;
}
这个方法的时间复杂度近乎O(n^2),甚至更高,因此这个算法实际运用中很少采纳,即使我们用了下面的方式来进行优化:
#include <iostream>
#include <cmath>
using namespace std;
const int maxn = 100005;
int num = 0 , a[maxn];
void is_prime(int n)
{
for(int i = 2; i < n; i++)
{
int ok = 1;
for(int j = 2; j < sqrt(i); j++) //对循环条件进行了优化
{
if(i % j == 0)
{
ok = 0;
break;
}
}
if(ok)
{
a[num++] = i;
}
}
}
int main(void)
{
int n;
cin >> n;
is_prime(n);
for(int i = 0; i < num; i++)
{
if(i < num - 1)
{
cout << a[i] << " ";
}
else
{
cout << a[i];
}
}
return 0;
}
这个算法面对大于等于1e + 6级数据规模时就会出现甚至肉眼可见的迟缓,这显然是难以接受的,因为在实际运用当中这种规模的数据实在是说不上高。
引述2
朴素素数筛:
这个方法是我从这位博主那里看到的,写得挺不错,尊重原创,有感兴趣的小伙伴可以传送过去看看:https://blog.****.net/PolarAurora/article/details/70187149
这种筛法的特点是事先排除掉了所有的偶数,从而实现运算的简化,但这种优化终归是有限的,说实话。
正文:
- 埃拉托斯特尼筛法
埃拉托斯特尼筛法,简称埃氏筛或爱氏筛,是一种由古希腊数学家埃拉托斯特尼所提出的一种简单检定素数时间复杂度为O(nlglgn)的算法。要得到自然数n以内的全部素数,必须把不大于根号n的所有素数的倍数剔除,剩下的就是素数。实现方法如下:
#include <iostream>
#include <cmath>
#include <cstring>
using namespace std;
const int maxn = 100000100;
int a[maxn] , flag;
bool judgement[maxn];
void is_prime(int n)
{
memset(judgement, 1, sizeof(judgement));
judgement[0] = 0;
judgement[1] = 0;
for(int i = 2; i < n; i++)
{
for(int j = 2; i * j <= n; j++)
{
judgement[i*j] = 0;
}
if(judgement[i])
{
a[flag++] = i;
}
}
}
int main(void)
{
int n;
cin >> n;
is_prime(n);
// cout << flag << endl;
for(int i = 0; i < flag; i++)
{
if(i < flag - 1)
{
cout << a[i] << " ";
}
else
{
cout << a[i];
}
}
return 0;
}
顺便验证了全局数组是可以开到1亿范围的……恐怖如斯……然而这个筛法在面对1e + 9以上的数据时也显得有些力不从心,跑了大概2-3s才出结果,这显然也不是最优的方法。
- 欧拉筛法
这是一种时间复杂度几乎为线性的优秀筛法,时间复杂度为O(n)。当然也稍微难理解些。
代码如下:
#include <iostream>
#include <cmath>
#include <cstring>
using namespace std;
const int maxn = 100000100;
int a[maxn] , flag;
bool judgement[maxn];
void is_prime(int n)
{
memset(judgement, 1, sizeof(judgement));
judgement[0] = 0;
judgement[1] = 0;
for(int i = 2; i <= n; i++)
{
if(judgement[i])
{
a[flag++] = i;
}
for(int j = 0; j < flag && i * a[j] <= n; j++)
{
judgement[i*a[j]] = 0;
if(i % a[j]==0) { break;} //保证每个合数被它最小的质因数筛去
}
}
}
int main(void)
{
int n;
cin >> n;
is_prime(n);
cout << flag << endl;
for(int i = 0; i < flag; i++)
{
if(i < flag - 1)
{
cout << a[i] << " ";
}
else
{
cout << a[i];
}
}
return 0;
}
仔细观察对比两种方法,我们可以发现在用埃式筛法时,同一个数字也许会被筛选多次,比如8先被2筛选一次,再被4筛选一次,这样就浪费了很多不必要的时间,而欧拉筛法通过
if(i%a[j] == 0) break; //a函数用来存储素数
这一步避免了重复筛选。
举个例子,比如,2先筛选了4,然后进行下一个循环,3筛选6和9,当我们执行到4的时候,可以发现,当i==4时,第一次运行到if(i%prime[j]==0)这一步的时候就直接break;掉了,这也就是说,当我们的合数进入循环时,其实它已经被之前的数筛选过了,所以当合数进入内层循环时,内层循环只执行了一次,从而减少了时间复杂度。本质上就是通过每次求出的最小素因子来进行运算,从而避免了对重复数字的多次筛选
对本文开头那道题目的解:
#include <iostream>
#include <cmath>
#include <cstring>
using namespace std;
const int maxn = 10000010;
int count , a[maxn] , flag;
bool judgement[maxn];
void is_prime(int n)
{
memset(judgement, 1, sizeof(judgement));
judgement[0] = 0;
judgement[1] = 0;
for(int i = 2; i <= n; i++)
{
if(judgement[i])
{
a[flag++] = i;
}
for(int j = 0; j < flag && i * a[j] <= n; j++)
{
judgement[i*a[j]] = 0;
if(i % a[j]==0) { break;} //保证每个合数被它最小的质因数筛去
}
}
}
int main(void)
{
int n;
cin >> n;
is_prime(n);
for (int i = 1; i < n; i++)
{
if(judgement[i])
{
count += 1;
}
}
cout << count << endl;
return 0;
}
当然,那位仁兄最后选择了希尔排序来做这道题,听说他用这个方法成功AC了,道不争不行,理不辨不明,我稍后将会用希尔排序做一下这道题,对比一下方法的复杂度,以上,感谢各位给予了我灵感和帮助的博主,感谢各位读者,欢迎在评论区提出批评建议,我将持续完善。谢谢!