文章是看了好多好多大佬的博客才自己总结出来的,有些地方觉得大佬的文字很精炼于是学习了一下,若是有人认为哪些地方我是不道德的,请指出,谢谢。
质数,也称素数,只包含两个因数,且一个因数为1,一个因数为它本身。
无论是数论、计算机应用上还是ACM上,质数都是基础且极其重要。于ACM上,质数经常用于判断一个数是否是质数或是枚举一个区间的质数。
现于ACM的需求初步学习质数,枚举1~n内的质数。
一、究极暴力法
从1枚举到n,计算能整除n的数的个数。复杂度O(n^2)。
二、暴力法优化
比2大的所有偶数一定不是质数。并且枚举可以只枚举到sqrt(n)即可,证明如下:
n = sqrt(n) * sqrt(n)
设 n = x * y,且 x >= sqrt(n),则必有 y <= sqrt(n)
那么意味着当我从小到大枚举的时候,若是找到了 y 便不需要找 x 了
故枚举到 sqrt(n) 足够了
证毕
#include
#include
using namespace std;
const int maxp = 700000;
int prime[maxp];
int main()
{
int n;
cin>>n;
int sub = 0;
prime[sub++] = 2; //2先存
for(int i = 3; i <= n; i++)
{
if(i % 2 == 0) continue; //偶数跳过
int j = 0;
for(j = 3; j * j <= i; j += 2) //枚举到sqrt(n)的所有奇数
{
if(i % j == 0) break;
}
if(j * j > i) prime[sub++] = i;
}
return 0;
}
复杂度O(n * sqrt(n))。若数据量不大的话可以用这种方法。
三、暴力法再优化
其实只枚举 3 到 sqrt(n) 的所有奇数也会出现重复判断的情况。
例如:枚举 3 到 sqrt(101) 的所有奇数为:3, 5, 7, 9,此处就会发现既然都不会整除 3 那么一定不会整除 9 !
故不仅仅是奇数,3 到 sqrt(n) 中之前记录的质数的整数倍也可以跳过。此时我们再讨论一下 3 到 sqrt(n) 之间有些什么数:偶数,奇数中质数的整数倍,质数,奇数中既不是质数又不是质数的整数倍的数。其实分析可得奇数中既不是质数又不是质数的整数倍的数是不存在的,证明如下:
设 y 是一个奇数,且 y 不是质数,且不是质数的整数倍
由 y 不是一个质数,假设 y = p1 * p2 * ... * pn
由 y 是奇数可知 pi ( 1 <= i <= n)必不为偶数,又 pi 也不为质数, 则 pi 也是和 y 一个定义,pi是一个奇数中既不是质数又不是质数的整数倍的数
再像处理 y 一样的处理 pi ,又可以得到一个 pi 的因子pii,同时 pii 也是一个奇数中既不是质数又不是质数的整数倍的数。。。无限循环下去
所以,这样的奇数中既不是质数又不是质数的整数倍的数是不存在的
证毕
那么会发现 3 到 sqrt(n) 我们只要尝试小于 sqrt(n) 的所有质数就可以了。
#include
#include
using namespace std;
const int maxp = 700000;
int prime[maxp];
int main()
{
int n;
cin>>n;
int sub = 0;
prime[sub++] = 2;
for(int i = 3; i <= n; i++)
{
if(i % 2 == 0) continue; //偶数跳过
int j;
for(j = 0; j < sub; j++)
{
if(i % prime[j] == 0) break;
}
if(j >= sub) prime[sub++] = i;
}
return 0;
}
复杂度再降,O(n * n之前的质数个数)。
四、筛法
筛法,顾名思义,不停的将合数筛出去,剩下的就只有质数。
先简单的说一下筛法的步骤:(筛 1 ~ n 的质数)
首先最小质数是 2,那么 1 ~ n 中所有的 2 的倍数全部去掉。剩下的数中最小的是 3 ,那么 3 是质数。再把 3 的倍数 都给去掉,剩下所有的数中最小的是 5,再去 5 的倍数。。。不断循环此过程,最后就可以把1 ~ n 之间的所有质数全部筛出来了。
证明不再给出,性质同上面证明(三、暴力再优化)。若还是不理解,请仔细分析质数的性质 “任一大于1的自然数,要么本身是质数,要么可以分解为几个质数之积,且这种分解是唯一的”。
#include
#include
#include
using namespace std;
const int maxn = 1000000;
const int maxp = 700000;
bool vis[maxn]; //筛数数组,0代表是质数,1代表不是质数
int prime[maxp]; //质数数组
int main()
{
int n;
cin>>n;
int sub = 0;
memset(vis, 0, sizeof(vis));
for(int i = 2; i <= n; i++)
{
if(!vis[i]) //如果是质数
{
prime[sub++] = i;
for(int j = i * 2; j <= n; j += i) //除去质数的整数倍
vis[j] = 1;
}
}
return 0;
}
效率大大优化,复杂度在O(n)与O(n * n之前的质数个数)之间。
五、筛法优化
通过模拟一次上面筛法,不难发现会出现许多重复的地方。
对应筛法的代码我们进行优化。j = i * i 优于 i * 2,举例说明,当 i = 2 时,我们已经把 vis[10] 赋值为 1 了,当 i = 5 时,vis[10]会再赋值一次。所以 j = i * i 会减少一部分重复。 再者 i 只到 sqrt(n) 便足够了,一个数 x 大于 sqrt(n),那么 x * x 一定大于 n,第二个循环都进不去,多余的判断。
给出优化后的代码:
/*
此处代码为白书《算法竞赛入门经典》的标准代码
*/
#include
#include
#include
using namespace std;
const int maxn = 1000000;
const int maxp = 700000;
bool vis[maxn];
int prime[maxp];
int main()
{
int n;
cin>>n;
int sub = 0;
memset(vis, 0, sizeof(vis));
int m = (int)sqrt(n); //避免浮点误差
for(int i = 2; i <= sqrt(n); i++)
{
if(!vis[i])
{
prime[sub++] = i;
for(int j = i * i; j <= n; j += i)
vis[j] = 1;
}
}
for(int j = sqrt(n) + 1; j <= n; j++)
if(!vis[j]) prime[sub++] = j;
return 0;
}
复杂度较暴力优化太多,别人还有很多更优化的算法。但我觉得既然是白书上的代码,已经很优秀,贪多嚼不烂,对于筛法的学习便到这了。
如果大佬发现了我的错误,也请你指出,谢谢!