一、素数筛
1.1 质数
什么是质数?
若a ,b 除±1外无其他公因数,则称a和b互质。
定理1(算术基本定理)
任意正整数n(n != 1)恰有一种方法写成质数的乘积(不计因数乘积的顺序)
定理2(欧几里得)
质数无穷多
1.2 试除法
那么我们初学任意编程语言时,一定会遇到求素数的问题,我们朴素的方法一般都是暴力枚举
inline bool isprime(int x)
{
for (int i = 2; i < x; i++)
if (x % i == 0)
return false;
return true;
}
1.3 根号优化
从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.4 埃氏筛
可这样仍然不能满足我们对于效率的要求,显然我们还要另寻出路。
空间换时间是我们算法优化中的一个重要思想,记忆化搜索就是该思想的一种应用
我们每判定出一个素数,对于这个素数乘上大于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 的 质因数的时候筛过了
这也是我们接下来进一步优化的思想
1.5 欧拉筛
其实这样我们的时间优化已经大大提升了,可是我们发现我们对于每个合数都筛选到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 <= x; i++)
{
if (!prime[i])
primes[cnt++] = i;
for (int j = 0; j < cnt && primes[j] * i <= x; 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],违反了筛法原则。
这样就保证了每个合数被最小质因数筛选一次
1.6 筛法求约数个数(补档)
整数n的约数个数计算公式:
很简单,所以不做证明。
逻辑很简单,具体筛法见代码
模板:
std::vector<int> minp, primes;
std::vector<int> cnt, d;
void sieve(int n) {
minp.assign(n + 1, 0);
primes.clear();
cnt.assign(n + 1, 0);
d.assign(n + 1, 0);
d[1] = 1;
for (int i = 2; i <= n; i++) {
if (minp[i] == 0) {
minp[i] = i;
primes.push_back(i);
cnt[i] = 1;
d[i] = 2;
}
for (auto p : primes) {
if (i * p > n) {
break;
}
minp[i * p] = p;
if (p == minp[i]) {
cnt[i * p] = cnt[i] + 1;
d[i * p] = d[i] / cnt[i * p] * (cnt[i * p] + 1);
break;
}
cnt[i * p] = 1;
d[i * p] = d[i] * 2;
}
}
}
1.7 筛法求约数之和(补档)
整数n的约数和计算公式:
我们筛法过程额外需要两个辅助数组 f , g
f[i] 存放 i 的约数和,g[i] 存放 i 的最小质因子 p 的 幂次和,即 1 + p^1 + p^2 + ...
逻辑很简单,具体筛法见代码
模板:
std::vector<int> minp, primes;
std::vector<int> f, g; // f = sum(因子) g = (最小素因子幂和, p^0 + ... p^x)
void sieve(int n) {
minp.assign(n + 1, 0);
f.assign(n + 1, 0);
g.assign(n + 1, 0);
primes.clear();
f[1] = 1;
for (int i = 2; i <= n; i++) {
if (minp[i] == 0) {
minp[i] = i;
primes.push_back(i);
f[i] = g[i] = i + 1;
}
for (auto p : primes) {
if (i * p > n) {
break;
}
minp[i * p] = p;
if (p == minp[i]) {
g[i * p] = g[i] * p + 1;
f[i * p] = f[i] / g[i] * g[i * p];
break;
}
g[i * p] = p + 1;
f[i * p] = f[i] * g[i * p];
}
}
}
1.8 筛法求欧拉函数(补档)
constexpr int N = 1E7;
bool isprime[N + 1];
int phi[N + 1];
std::vector<int> primes;
std::fill(isprime + 2, isprime + N + 1, true);
phi[1] = 1;
for (int i = 2; i <= N; i++) {
if (isprime[i]) {
primes.push_back(i);
phi[i] = i - 1;
}
for (auto p : primes) {
if (i * p > N) {
break;
}
isprime[i * p] = false;
if (i % p == 0) {
phi[i * p] = phi[i] * p;
break;
}
phi[i * p] = phi[i] * (p - 1);
}
}