素数筛
什么是质数?
若a ,b 除±1外无其他公因数,则称a和b互质。
定理1(算术基本定理)
任意正整数n(n != 1)恰有一种方法写成质数的乘积(不计因数乘积的顺序)
定理2(欧几里得)
质数无穷多
那么我们初学任意编程语言时,一定会遇到求素数的问题,我们朴素的方法一般都是暴力枚举
inline bool isprime(int x) { for (int i = 2; i < x; i++) if (x % i == 0) return false; return true; }
从2枚举到 x - 1,如果整除那么就不是素数
代码的正确性毫无疑问 , 但是如果需求量过大,效率就显得低下
这时有人会发现,我们没必要枚举到x - 1,我们只需要枚举到它的平方根即可
(小的那个因数整除,大的必然整除)
如此一来,代码就变成了这个样子
inline bool isprime(int x) { int sq = (int)sqrt(x); for (int i = 2; i < sq + 1; i++) if (x % i == 0) return false; return true; }
优化掉sqrt的二分快速幂的复杂度的话
inline bool isprime(int x) { for (int i = 2; i <= x / i; i++) if (x % i == 0) return false; return true; }
可这样仍然不能满足我们对于效率的要求,显然我们还要另寻出路。
空间换时间是我们算法优化中的一个重要思想,记忆化搜索就是该思想的一种应用
我们每判定出一个素数,对于这个素数乘上大于1的因子的数显然都是合数,如果我们能标记这些合数,我们就能省略去许多
不必要的判断
const int maxn = 1e7 + 10; int prime[maxn + 1]; inline bool isprime(int x) { prime[2] = 0; for (int i = 2; i <= maxn / i; i++) { if (!prime[i]) for (int j = i * i; j <= maxn; j += i) prime[j] = 1; } return !prime[x]; }
这就是我们的埃氏筛
这里解释为什么埃氏筛的j要从i * i而不是2 * i开始, 因为对于大于2的i , 任意 k * i(k < i)它一定在筛k 的 质因数的时候筛过了
这也是我们接下来进一步优化的思想
其实这样我们的时间优化已经大大提升了,可是我们发现我们对于每个合数都筛选到x这样一定有重复的情况
比如对于36 , 它可以被4筛 可以被9筛等等,就像kmp算法next数组优化为nextval一样, 我们的埃氏筛也有需要优化的地方
我们先上代码
const int maxn = 1e7 + 10; int primes[maxn + 1], cnt = 0;//0为素数 1为合数 int prime[maxn + 1]; inline bool isprime(int x) { prime[2] = 0; for (int i = 2; i <= maxn; i++) { if (!prime[i]) primes[cnt++] = i; for (int j = 0; j <= cnt && primes[j] * i < maxn; j++) { prime[primes[j] * i] = 1; if (i % primes[j] == 0) break; } } return !prime[x]; }
这就是素数筛的终极版--欧拉筛
那么为什么欧拉筛能保证每个合数都被筛且只被筛一次呢
还记得我们的定理1(算术基本定理)吗
定理1(算术基本定理)**
任意正整数n(n != 1)恰有一种方法写成质数的乘积(不计因数乘积的顺序)
欧拉筛法核心思想:每个合数只被自己的最小质因子筛一次。
我们prime[i]是从最小的素数开始枚举
i%prime[j] != 0 则说明prime[j]就是我的最小质因数
对 i%prime[j]==0 时即跳出循环的解释:此时可以令 i = k×prime[j],若不跳出,继续筛 i×prime[j+1] 的话,i×prime[j+1] = k×prime[j]×prime[j+1],它的最小质因子显然是 prime[j] 而不是 prime[j+1],违反了筛法原则。
这样就保证了每个合数被最小质因数筛选一次