筛选质数——埃及筛及其他筛法
埃拉托斯特尼筛法,又称埃氏筛。
这种筛法是朴素筛法(即线性筛法)的优化版,可以提高筛选效率。
其核心思想为:某个质数乘以x(x为大于2的正整数)后,这个数必为合数。
下面举例说明:
题目:计算n以内的质数个数。
面对这一要求,我之前的第一反应是暴力求解,遍历从2到n的所有数,然后计算出符合要求的数的个数。
但这一操作在很多时候都会加大代码的时间复杂度,导致超时。
如果用到埃氏筛,就可以将时间复杂度优化到O(nloglogn),可粗略看作O(n)。方法如下:
创建一个大小为n+1的数组judge 初始化全为0,0表示质数。
首先从2开始遍历每一个数,遍历终点为sqrt(n)。若该数judge[i]为0(即i是质数),则将judge[i x 2]开始到 judge[i x i]全部改为1,1表示合数。
因为每一个合数都可以写成几个质数的积,那么所有质数的倍数就是2—n内的所有合数,每一个合数都会被比他小的质因数筛掉,这就是埃氏筛(注意从2开始遍历,1既不是质数也不是合数!)。
在学习埃氏筛的同时,也了解了与质数相关的其他算法,在这里粗略介绍:
- 试除法分解质因数–O(logN)~O(sqrt(N))
原理:从[2,sqrt(n)]中枚举所有的质数,如果找到某一个质 数i可以将n整除,则需要将n连续除以i得到m个i,直到无法整除为止,然后将n中去除m个i的数,继续操作,如果最后一个数大于1,则得到最后一个质因子。
void divide(int x)
{
for (int i = 2; i <= x / i; i ++ )
if (x % i == 0)
{
int s = 0;
while (x % i == 0) x /= i, s ++ ;
cout << i << ' ' << s << endl;
}
if (x > 1) cout << x << ' ' << 1 << endl;
cout << endl;
}
这个代码这么写是因为任意一个大于1的正整数都可以分解成一个或几个质数相乘,如36可分解为2乘2乘3乘3,几个因数从小到大排列,相同的项可以合并成平方的形式,因此我们可以说任意一个正整数都可以分解为一个或几个质数的平方积。
- 欧拉筛法O(N)
这一筛法是埃氏筛法的进阶版本,拥有更低的时间复杂度。
出现这一结果的原因是埃氏筛将同一个合数重复筛除了。比如12,它既是2的倍数,又是3的倍数,被重复筛了两遍,从而影响了效率。
而欧拉筛法则可以有效优化这一问题。
这部分内容较难,我先借用一段其他博主的代码:
bool isprime[MAXN]; // isprime[i]表示i是不是素数
int prime[MAXN]; // 现在已经筛出的素数列表
int n; // 上限,即筛出<=n的素数
int cnt; // 已经筛出的素数个数
void euler()
{
memset(isprime, true, sizeof(isprime)); // 先全部标记为素数
isprime[1] = false; // 1不是素数
for(int i = 2; i <= n; ++i) // i从2循环到n(外层循环)
{
if(isprime[i]) prime[++cnt] = i;
// 如果i没有被前面的数筛掉,则i是素数
for(int j = 1; j <= cnt && i * prime[j] <= n; ++j)
// 筛掉i的素数倍,即i的prime[j]倍
// j循环枚举现在已经筛出的素数(内层循环)
{
isprime[i * prime[j]] = false;
// 倍数标记为合数,也就是i用prime[j]把i * prime[j]筛掉了
if(i % prime[j] == 0) break;
// 最神奇的一句话,如果i整除prime[j],退出循环
// 这样可以保证线性的时间复杂度
}
}
}
原文链接:https://blog.csdn.net/qaqwqaqwq/article/details/123587336
这段代码在我初学时给予了我极大的帮助。
通俗易懂地给大家讲讲我自己的思路,大家看完后结合代码理解就会容易得多:
首先,我们定义两个数组:isprime和prime。前者用于标识2到n这些有序排列的数是否为质数,由于是布尔量,只有是或不是两种情况。后者则是用于保存筛出来的质数。
然后来看函数里面,先用memset函数将数组isprime全标记为质数(1除外),然后外层for循环会依次遍历2到n的每一个数(这每一个数都已经被标记为质数了)
当i=2时,isprime[i]的值为ture(之所以为ture是因为之前将2到n的所有数都标记为了质数,而不是因为2本来是质数,这里并没有对2这一自然数本身作判断,而是只针对isprime[2]作了判断),意思是2此时被标记为质数,满足if条件,将i=2放到数组prime中。
下一行代码是用来筛掉i的素数倍,即(i乘以一个素数的值)被筛去,具体操作如图,我们写一个for循环,遍历1到cnt的每一个数,cnt为放进prime数组中质数的个数。
仔细看,就会发现其实这个循环的判断条件是i乘以prime中每一个质数的结果是否小于右边界n,如果小于,进入循环,将本次i乘以prime[j]标记为false(本来为ture)。
接下来是欧拉筛法最核心的部分:如果i%prime[j]等于0,跳出循环。用数学思维翻译下,意思是说如果此时与i相乘的prime中的质数是i的最小质因数(之所以为最小,是因为prime中的质数是以从小到大顺序排列,当遇到第一个质因数就跳出循环,这第一个质因数就是他的最小质因数了),就跳出内层循环,直接开始下一次外层循环。
给大家纸上调试代码捋一捋:
一、我最开始看2,由于被标记为质数,就将他放进prime中(此时是放到prime[1]中),然后用2乘以prime[1],得到4,由于4<n,所以进入内层循环,将4标记为合数,因为2%2等于0,跳出内层循环,开始下一次外层循环。
二、外层++i等于3,由于也被标记为质数,所以将3放进prime[2]中,然后看内层循环,3乘以prime[1]即3乘以2,得到6,由于6<n,进入内层循环,将6标记为合数,因为3%2不等于0,所以不执行break,进入到下一次内层循环中。3乘以prime[2],得到3乘以3==9,由于9<n,进入循环,将9标记为合数,因为3%3等于0,跳出内层循环,开始下一次外层循环。
三、外层++i等于4,由于它被标记为合数,不被放入prime中,但仍进行循环,4乘以prime[1]得到8,8<n,进入内层循环,将8标记为合数。注意:此时i%prime[1]等于0,直接跳出内层循环开始下一次外层循环,此时i等于5,而i等于4,j等于2的情况被直接跳过了,因为prime[1]是4的最小质因数。
如果我们进行了i=4,j=2这一次循环,即将4乘以3所得的12标记为合数的话,后面i=6时,i*prime[1]又会重复标记一遍12,导致时间复杂度变高。
这就是欧拉筛法,其特点是每一个合数都是被他的最小质因数给筛掉的,大大降低了时间复杂度。
但有得亦有失,时间复杂度降低的代价是时空间复杂度的升高,毕竟创建了两个数组,所以我们使用时一定要结合具体要求,不能盲目追求最优解。