概念
质数,又名素数,是只能被1或者自身整除的自然数(不包括1)。
判断是否是质数最直观和简单的方法就是从2开始直接除,能除尽(余数为0)就不是质数。
整数的唯一分解定理
任何一个大于$1$的自然数N,如果N不为质数,都可以唯一分解成有限个质数的乘积:$N = P1^{a1} * P2^{a2} * P3^{a3} * … * Pn^{an}$,这里$P1 < P2 < P3 <…< Pn$均为质数,指数$ai(1 le i e n)$是正整数。
(质数的话,直接就是它本身)
代码实现(C)
简单粗暴
int isprime(int N) { int i; for(i = 2;i < N;i++) { if(N % i == 0) return 0; else return 1; } }
该算法的时间复杂度$O(N)$。
优化一:缩小范围
可以改进一下,根据如果一个数是合数,那么它的最小质因数肯定小于等于它的平方根。用反证法可以证明一下。假设$x$是$n$的最小质因数,则存在$n / x = p$。$p > x$,$x * p = n$。如果$x$不小于等于它的平方根,则$x * x > n$,而$p > x$,故$x * p > n$,假设不成立。合数是与质数相对应的自然数。一个大于$1$的自然数如果它不是合数,则它是质数。也就是说如果一个数能被它的最小质因数整除的话,那它肯定是合数,即不是质数。所以判断一个数是否是质数,只需判断它是否能被小于它开根号后的所有数整除,因此,这样做的运算少了很多,降低了时间复杂度。
代码实现(C)
int is_prime(int N) { int i; for(i = 2;i * i <= N; i++) { if(N % i == 0) return 0; else return 1;
} }
时间复杂度$O(sqrt{N})$。
优化二:埃拉托色尼(Eratosthenes)筛选法
上面两种方法是最常见,也最简单的。如果要判断一个范围内的数是否是质数,则需要多次调用 is_prime() 函数。比如N范围内(2~N)的质数枚举,则时间复杂度变为$O(N * N)$或$O(N * sqrt{N}$。这时,可以采用另一种称为“埃拉托色尼(Eratosthenes)筛法”的方法。埃拉托色尼是古希腊的著名数学家。他采取的方法是,在一张纸上写上1到100全部整数,然后逐个判断它们是否是质数,找出一个非质数,就把它挖掉,最后剩下的就都是质数了。具体做法如下:
- <1> 先把$1$删除(因为$1$既不是质数,也不是合数)。
- <2> 读取队列中当前最小的数$2$,然后把$2$的倍数删去。
- <3> 读取队列中当前最小的数$3$,然后把$3$的倍数删去。
- <4> 读取队列中当前最小的数$5$,然后把$5$的倍数删去。
- <5> 如上所述直到需求的范围内所有的数均删除或读取。
埃氏筛法的思想可总结为:对于不超过$N$的质数$p$,依次筛除它的倍数:$2p, 3p, 4p…$,当处理完所有的数之后,剩下没被筛掉的数就都是质数。
这是个典型的用空间换时间的算法,计算N范围内质数的时间复杂度为$O(sqrt{N} * sqrt{sqrt{N}})$。
代码实现(C)
//埃式筛法 int flag[N + 1] = {0}; //质数为0 void is_prime() { //0和1既不是质数,也不是合数 flag[0] = 1; flag[1] = 1; for (int i = 2;i * i <= N; i++) { if (flag[i] == 0) { for (int j = i * i; j <= N; j += i) { // i * 2 => i * i的优化,去掉重复筛选 flag[j] = 1; } } } } //函数执行后,flag值等于0对应的数即为N范围内的质数
【优化: j = i * i
因为$2 * i, 3 * i, 4 * i, … , (i - 1) * i$肯定已经被前面的$2, 3, 4, … , (i - 1)$中包含的所有质数及其倍数合数给筛掉了,所以说从$i * i$开始即可。
详细点说,我们筛掉的是质数$i$的倍数合数(n * i),那么其中比$i$倍小的那些倍数,如$2, 3, … i-1$,在$i$作为素数开始筛后面合数之前,肯定已经被当作$2, 3, … i-1$这些数中的素数或其倍数合数给筛掉了,所以我们只需要从 j = i * i 开始即可。】
优化三:线性筛(欧拉筛法)
埃氏筛法无法避免重复地筛一些数,比如说$30 = 2 * 15 = 3 * 10 = 5 * 6$,所以为了避免重复筛数,我们有线性筛!
线性筛是在埃氏筛法的基础上,让每一个合数,只被它的最小质因子筛选一次,达到不重复的目的。
根据整数的唯一分解定理,每个合数有且只有一组质数相乘可以得到。前面的埃氏筛法,每一个质因子都要筛掉该合数一次,我们如果让每个合数只被它最小的质因子筛选就完全避免了埃氏筛法的重复。从算法来说,N范围内的每个数都只会被筛一次,所以算法复杂度是O(N)。
代码实现(C)
int flag[N + 1] = {0}; int prime[N + 1]; //prime是用来存质数的数组,显然数组中的质数是从小到大 int cnt = 0; void is_prime() { flag[0] = 1; flag[1] = 1; for (int i = 2; i <= N; i++) { if (flag[i] == 0) prime[cnt++] = i; for (int j = 0; j < cnt; j++) { if (i * prime[j] > N) break; flag[i * prime[j]] = 1; if (i % prime[j] == 0) break; //筛到i的最小质因数即可,后面的都是重复 } } }
【注解:
- 我们知道一个整数$n$的最大质因子不超过$sqrt_n$,又因为我们的prime[ ]数组内的质数是单调递增的,所以我们可以保证对于外层for循环跑到$i$的时候,它的最大质因子已经存在于prime[ ]中了。所以我们也一定能跑到它最小的质因子。
- 关于 flag[i * prime[j]] = 1; 的解释: i * prime[j] 也就是质数 prime[j] 作为最小质因子能够筛掉的部分合数。这里我们跑的是$j$的循环,也就是把prime[ ]中记录的质数,升序来当作要消去合数的最小质因子,所以是对于整数$i$(无论$i$是合数还是质数), i * prime[j] 都是合数会被筛掉。
- 举个例子:$i = 6$,$6$的最小质因子是$2$,这时候我们可以筛掉$6 * 2 = 12$,然后就结束了。如果不结束,接着下一个质数是$3$,那么我们筛掉的就是$18 = 6 * 3 = 2 * 3 * 3$(这里显然$3$不是18的最小质因子,违背了我们的算法)。$18$的最小质因子是$2$,那么$18$就会被重复筛(当$i = 9$时,$9$的最小质因子是$3$,会筛掉$9 * 2 = 18$,$9 * 3 = 27$)。至于$i$本身就是质数,那么它的最小质因子就是它本身。
- 所以这就是为什么 if(i % prime[j] == 0) 会存在了,我们必须保证只跑到$i$的最小质因子就结束。因为我们必须保证 prime[j] 是 i * prime[j] 的最小质因子才行!】
(整理自网络)
典型应用
区间质数枚举
问题描述
给定整数$a$和$b$($a < b$),枚举区间$[a, b)$内的质数。
解题思路
因为$b$以内合数的最小质因数一定不超过$sqrt{b}$,如果有$sqrt{b}$以内的质数表的话,就可以把筛选法用在$[a, b)$上了,先分别做好$[2, sqrt{b})$的表和$[a, b)$的表,然后从$[2, sqrt{b})$的表中筛得质数的同时,也将其倍数从$[a, b)$的表中划去,最后剩下的就是区间$[a, b)$内的质数了。
代码实现(C)
//区间质数 #include <stdio.h> #include <stdlib.h> #define max(a, b) (((a) >= (b)) ? (a) : (b)) int main() { int a, b; scanf("%d%d", &b, &a); int *flag = (int *)calloc(b + 1, sizeof(int)); flag[0] = 1; flag[1] = 1; int *prime = (int *)calloc(b - a + 1, sizeof(int)); for (int i = 2; i * i <= b; i++) { if (flag[i] == 0) { for (int j = i * i; j <= b; j += i) flag[j] = 1; //i * 2 => i * i的优化,去掉重复筛选 for (int k = max(2, (a + i - 1) / i) * i; k <= b; k += i) { prime[k - a] = 1; } } } for (int i = 0; i <= b - a; i++) { if (prime[i] == 0) printf("%d ",i + a); } return 0; }
区间约数枚举
问题描述
正整数$x$的约数是能整除$x$的正整数。正整数$x$的约数个数记为$div(x)$。例如,$1, 2, 5, 10$都是正整数$10$的约数,且$div(10) = 4$。设$a$和$b$是$2$个正整数,$a le b$,找出$a$和$b$之间约数个数最多的数$x$。
设正整数$x$的质因子分解为$x = p1^{N1} * p2^{N2} * …… * pi^{Ni}$, 则$div(x)=(N1+1)(N2+1)……(Ni+1)$。
解题思路
vector<int> gg(int n) { vector<int> a; for(int i=2;i*i<=n;i++){ if((n%i)==0){ a.push_back(i); if((n/i)!=i)a.push_back(n/i);//根号n的情况不要重复添加 } } return a; }
(整理自网络)
参考资料:
https://blog.csdn.net/hebtu666/article/details/81486370
https://blog.csdn.net/weixin_44049850/article/details/95310444
https://blog.csdn.net/serena_0916/article/details/55044832