打算写一下自己对ACM中常用到的数论知识,加深巩固和查缺补漏。
在数学中,对于n>1,若n的因子只有1和它本身,那么n就是质数,也称作素数。而在ACM中素数类型的题目是经常会碰到的。
那么我们可以怎么判断素数呢?
一.对于判断单个n是否为素数,我们可以通过三种情况来判断:
(1)用i遍历2~ n -1,若n % i == 0就代表存在除1和它本身以外的因子,就直接跳出,若执行完就代表没有其它因子,n为素数。时间复杂度为O(n)
(2) 我们可以优化(1),因为若存在大于sqrt(n)的因子k,那么n / k 的值一定在 2 ~ sqrt(n)中。因此我们只需要用i遍历2 ~ sqrt(n)即可,若n%i == 0就代表存在除1和它本身以外的因子,就直接跳出,若执行完就代表没有其它因子,n为素数,时间复杂度为O(sqrt(n)) 。
(3) 我们继续优化(2),若 n % 2 != 0,那么从i从3~sqrt(n),只需要考虑奇数即可。因此时间复杂度为O(sqrt(n) / 2)
我给出(3)的代码:
bool chooes(ll n) { //判断n是否为素数
if (n % 2 == 0) return false; //若n%2==0,则说明n不是素数
for (int i = 3; i * i <= n; i += 2) { //从3~sqrt(n),每次+=2,若存在n % i == 0,则说明n不是素数
if (n % i == 0) return false;
}
return true; //否则n为素数
}
二.对于判断2~n每个数是否为素数,我们应该怎么办呢?
我们利用一中最快的(3)方法,时间复杂度为O(n✖️sqrt(n) / 2),若n为1e5以上,1s之内是筛选不出来的。因此我们只能想别的办法,先来介绍第一种:
(1)由于是求2~n的每个数,因此我们可以尝试利用之前已经判断过的数来求后面的数是否为素数。我们知道,素数的因子只有1和它本身,那么一个2以上的数的倍数一定不是素数!!又因为合数一定可以由素数累乘得到,所以我们只需要把所有素数的倍数标记为合数,剩余的数一定是质数。
也就是说,对于n = 10,素数2的倍数4,6,8,10一定是合数;
素数3的倍数6,9一定是合数
4是合数跳过,素数5的倍数10一定是合数;
6是合数跳过,得到素数7
由于8,9,10是合数,因此全部跳过
我们就得到10以内的素数2,3,5,7
通过这样的办法,我们可以在趋近于线性(实际是nlog(log(n))) 的时间内求出2~n中的每个数是否是素数。
以上的第一种方法,是被埃拉托斯特尼发明的,因此也叫做埃筛法
如果大家觉得还不够快,趋近于线性也不是线性啊!!有没有线性时间就能求出来2~n所有素数的方法呢?
答案是有的,该方法称为线筛。
(2)我们知道上面的数,对于素数2的倍数4,6,8,10一定是合数;
对于素数3的倍数6,9一定是合数;
大家有没有发现6此时两次被判定为合数,埃筛之所以不是线性的原因就在于这里。。它可能会造成合数的重复判定。我们怎么样才能每个数只判定一次呢?就是在这个合数的最小素因子时判定该数为合数,其它素因子时不判定,这样就可以保证每个合数只判定一次了。这也就是线性筛的精髓了:
对于2,我们判定为素数,我们检索已经存在的素数2,把2✖️2,也就是4标记为合数;
对于3,我们判定为素数,我们检索已经存在的素数2, 3,把2✖️3,3✖️3,也就是6, 9标记为合数
对于4已标记为合数,我们检索已经存在的素数2,3,把2✖️4,也就是8标记为合数,由于4 % 2 == 0,因此我们直接跳出,不再执行3✖️4也就是12的判定(因为它可以由2✖️6判定,2是12的最小素因子)
对于5,我们判定为素数,我们检索已经存在的素数2,3,5,把2✖️5,3✖️5,5✖️5,也就是10,15,25标记为合数
对于6已标记为合数, 我们检索已经存在的素数2,3,5,把2✖️6,也就是12标记为合数,由于6 % 2 == 0,因此我们直接跳出,不再执行3✖️6也就是18的判定(因为它可以由2✖️9判定,2是18的最小素因子)
………
就不往下继续写了,通过以上的流程,我们就可以判定2~n中的每个数是否为素数并保证每个数只判定一次(还可以加如果两数相乘大于n直接跳出等限制条件),这样时间复杂度就是O(n)
我给出(2)方法的实现代码:
#include
#include
#define ll long long
#define N 100005
bool book[N + 5]; //book[i]判断i是否为素数
int prime[N + 5]; //prime数组存储2~N全部素数,若MLE可改小该数组长度
void init() {
memset(book, 0, sizeof(book));
prime[0] = 0; //代表2~N当前有多少个素数
for (int i = 2; i <= N; i++) {
if (!book[i]) { //如果是素数
prime[++prime[0]] = i; //存入该素数
}
for (int j = 1; j <= prime[0] && (ll)prime[j] * i <= N; j++) { //遍历之前存储的素数
book[prime[j] * i] = 1; //素数的i倍一定不是素数
if (i % book[j] == 0) break; //保证每个合数只判断一次的精髓
}
}
return;
}
int main () {
init();
return 0;
}
以上就是素数判定和素数筛的一些理论与模版。在ACM中关于素数的题目很活,变形也很多,大家有兴趣可以看一下我博客中的数论标签的题目,里面有很多基于素数筛框架的变形题目~
附上博客地址:跳转
转载请注明出处!!!
如果有写的不对或者不全面的地方 可通过主页的联系方式进行指正,谢谢