数论之质数筛法
基本定义及性质:
质数又称素数。一个大于1的自然数,除了1和它自身外,不能被其他自然数整除的数叫做质数;否则称为合数。
在整个自然数集合中,质数的数量不多,分布比较稀疏,对于一个足够大的整数N,不超过N的质数大约有 N/lnN 个,即每lnN个数中有一个质数。
质数的判断:
有一个简单而又暴力的算法,即试除法, 即扫描 2~ sqrt(N) 之间的所有整数,依次检查它们能否整除N。
伪代码如下:
1 bool is_prime(int n)
2 {
3 if(n < 2) return false;
4 for(int i = 2;i <= sqrt(n);i++)
5 if(n%i == 0) return false;
6 return true;
7 }
时间复杂度为: O(sqrt(N))。
有一些效率更高的随机性算法,不过水平超出了我们的范畴,不再讨论。
质数的筛选:
质数的筛法有许多种,先来介绍一个最实惠常用的埃拉托色尼斯筛法吧。
我们可以从2开始, 由小到大扫描每个数x,它的倍数 2x,3x,....,[N/x] * x 标记为合数。
当扫描到一个数时,若它尚未被标记,则它不能被2~x - 1之间的任何数整除,该数就是质数。
伪代码如下:
1 void primes(int n)
2 {
3 memset(vis, 0, sizeof(vis));
4 for(int i = 2;i <= n;i++)
5 {
6 if(vis[i]) continue;
7 printf("%d\n", i); //i是质数
8 for(int j = i;j <= n/i;j++) vis[i*j] = 1;
9 }
10 }
时间复杂度为:O(NloglogN);
该算法实现简单,效率已经非常接近于线性,是算法竞赛中最常用的质数筛法。
再来介绍一种更优的算法:线性筛(欧拉筛法)
算法思想:通过"从大到小累积质因子"的方式标记每个合数,设数组 v 记录每个数的最小质因子,我们按以下步骤维护 v;
1.依次考虑 2~N 之间的每一个 i.
2.若v[i] = i, 说明 i 是质数, 把它保存下来。
3.扫描不大于 v[i] 的每个质数, 令 v[i*p] = p. 也就是在 i 的基础上累积一个质因子 p,因为 p <= v[i],所以p就是合数 i*p 的最小质因子。
伪代码如下:
1 void primes(int n)
2 {
3 memset(v, 0, sizeof(v));//最小质因子
4 int m = 0;//质数数量
5 for(int i = 2;i <= n;i++)
6 {
7 if(v[i] == 0)
8 {
9 v[i] = i;
10 prime[++m] = i;//i是质数
11 }
12 //给当前的数i乘上一个质因子
13 for(int j = 1;j <= m;j++)
14 {
15 // i有比prime[j]更小的质因子,或者超出n的范围,停止循环。
16 if(prime[j] > v[i] !! prime[j] > n/i) break;
17 //prime[j]是合数i*prime[j]的最小质因子
18 v[i*prime[j]] = prime[j];
19 }
20 }
21 }
其实还可以利用欧拉筛法顺带求出欧拉函数,这里先提供伪代码。
伪代码如下:
1 void eular(int n)
2 {
3 //数组phi 表示欧拉函数
4 int cnt = 0;
5 for(int i = 2;i <= n;i++)
6 {
7 if(v[i] == 0)
8 {
9 prime[++cnt] = i;phi[i] = i-1;
10 }
11 for(int j = 1;j <= cnt;j++)
12 {
13 if(prime[j]*i > n) break;
14 v[prime[j]*i] = 1;
15 if(i%prime[j] == 0)
16 {
17 phi[i*prime[j]] = phi[i]*prime[j];
18 break;
19 }
20 else
21 phi[i*prime[j]] = phi[i]*(prime[j]-1);
22 }
23 }
24 }