线性筛积性函数
0. 前言
积性函数是数论中一种极其重要的函数。它是指对于一个函数 \(f(x)\),如果 \(\gcd(x, y) = 1\),则 \(f(xy) = f(x)f(y)\),则 \(f(x)\) 就是一个积性函数。
积性函数大多数可以用线性筛质数的方法筛出来,本文将介绍几种常见的积性函数的筛法及一些拓展。
1. 线性筛质数
大佬可跳过这一部分内容。
如果我们要判断一个数 \(n\) 是不是质数,那么很朴素的想法就是 \(\Theta(\sqrt n)\) 的复杂度找 \(2 \sim \sqrt n\) 中有没有 \(n\) 的约数。
这是对于单独求一个数来说最优的解法。但如果有若干个数,你要求每个数是不是质数,\(\Theta(n\sqrt n)\) 那就显得有些慢了。所以伟大的数学家埃拉托斯特尼发明了一种 \(\Theta(n\ln \ln n)\) 的筛法——埃氏筛法。
很显然的是,两个大于 \(1\) 的整数的乘积一定不是质数。埃氏筛法就是根据这一点筛质数的。
首先枚举外层循环的一个数 \(i\),然后把 \(i\) 的倍数全部筛掉,这样枚举完所有的 \(i\) 后,剩下的数就一定都是质数了。
按照这样的思路,可以写出下面的代码:
int p[N], cnt;
bool st[N];
void prime(int n)
{
for (int i = 2; i <= n; i ++ )
{
if (!st[i]) p[ ++ cnt] = i;
for (int j = 2; i * j <= n; j ++ )
st[i * j] = true;
}
}
你可能会说,这算法时间复杂度不是 \(\Theta (n \log n)\) 吗,怎么就是 \(\Theta(n\ln \ln n)\) 了?
没错,这份代码并不是理想的 \(\Theta(n\ln \ln n)\),我们要对其进行优化。
容易发现的是,对于一个数 \(x\),如果它有小于 \(x\) 的某个质因子,那么它一定是合数。
所以我们可以不用对于每一个数 \(i\) 都去寻找它的倍数,只需要找所有质数的倍数即可。
int p[N], cnt;
bool st[N];
void prime(int n)
{
for (int i = 2; i <= n; i ++ )
if (!st[i])
{
p[ ++ cnt] = i;
for (int j = 2; i * j <= n; j ++ )
st[i * j] = true;
}
}
*其实在这份代码的内层循环中,\(j\) 的初始值可以赋成 \(i\),这样也算是一个小优化。但效果微乎其微,且与本文主旨不符,遂不作说明。
这样代码的时间复杂度就是 \(\Theta(n\ln \ln n)\) 了。具体的为什么是这个值去问数学老师吧。
但是,这就结束了吗?不是说要线性筛质数吗?接下来就是伟大的数学家欧拉创造的线性筛质数。
容易发现,对于上面的埃筛,一个数可能会被筛掉多次。比如 \(12\) 会被 \(2\) 筛掉,又会被 \(3\) 筛掉,这样的重复操作会使时间复杂度带上一个 \(\ln\ln n\)。
怎么优化这一点呢?我们只需要让每个数只被它的最小质因子筛掉就行了。
先放出代码,再进行解释。
int p[N], cnt; // p[i] 表示第 i 个质数
bool st[N]; // st[i] 表示 i 是否是质数
void prime(int n)
{
for (int i = 2; i <= n; i ++ )
{
if (!st[i]) p[ ++ cnt] = i;
for (int j = 1; p[j] <= n / i; j ++ )
{
st[i * p[j]] = true;
if (i % p[j] == 0) break;
}
}
}
如果 \(i\) 之前被 \(p_j\) 筛过了,又由于 \(p\) 里面质数是从小到大的,所以 \(i\) 乘上其他的质数的结果一定会被 \(p_j\) 的倍数筛掉,就不需要在这里先筛一次,所以这里直接 break
。
这样时间复杂度就变成了 \(\Theta(n)\)。
以上内容异常重要,是接下来筛其它积性函数的基础。
2. 线性筛欧拉函数
一句介绍:\(\varphi(x) = \sum\limits_{k = 1}^x[\gcd(k, x) = 1],\)表示 \(1 \sim x\) 中与 \(x\) 互质的数。
一句求解:\(\varphi(n) = n \times \prod\limits_{p \mid n, p\ is\ a\ prime} \dfrac {p-1}p\)。
现在讨论怎么在线性筛质数把欧拉函数也求出来。
- 若 \(i\) 是质数,显然 \(1 \sim i-1\) 都与 \(i\) 互质,故 \(\varphi(i) = i - 1\);
- 枚举 \(j\),找到 \(p_j \times i\),分两种情况求解 \(\varphi(p_j \times i)\):
- 若 \(i \bmod p_j = 0\),也就是 \(p_j\) 是 \(i\) 的最小质因子。那么也就是说在原来的 \(i\) 中就已经有了一个质因子 \(p_j\),所以 \(i\) 和 \(i \times p_j\) 的质因子是相同的。假设 \(i\) 的所有质因子是 \(q_k\),那么 \(i \times p_j\) 的质因子也是 \(q_k\),所以有 \(\varphi(p_j \times i) = p_j \times i \times \prod\limits_{q \mid i, q\ is\ a\ prime}\dfrac{q-1}q\)。其中后面的 \(\prod\limits_{q \mid i, q\ is\ a\ prime}\dfrac{q-1}q\) 就是 \(\varphi(i)\) 的值,所以 \(\varphi(p_j \times i) = p_j \times \varphi(i)\);
- 若 \(i \bmod p_j \ne 0\),也就是 \(p_j\) 不是 \(i\) 的最小质因子。那么 \(i \times p_j\) 的质因子就应该比 \(i\) 多一个 \(p_j\),所以在算 \(p_j\) 给 \(\varphi(i \times p_j)\) 的贡献时应该 \(\times \dfrac{p_j-1}{p_j}\),所以 \(\varphi(i \times p_j) = p_j \times \varphi(i) \times \dfrac{p_j-1}{p_j} = \varphi(p_j \times i) = (p_j - 1) \times \varphi(i)\)。还可以根据积性函数的定义,故 \(\varphi(p_j \times i) = \varphi(p_j) \times \varphi(i) = (p_j - 1) \times \varphi(i)\)。
int p[N]; // p[i] 表示第 i 个质数
bool st[N]; // st[i] 表示 i 是否是质数
int phi[N]; // phi[i] 表示 φ(i) 的值
int cnt;
void prime(int n)
{
phi[1] = 1;
for (int i = 2; i <= n; i ++ )
{
if (!st[i]) p[ ++ cnt] = i, phi[i] = i - 1;
for (int j = 1; p[j] <= n / i; j ++ )
{
st[p[j] * i] = 1;
if (i % p[j]) phi[i * p[j]] = phi[i] * p[j]; // p[j] 不是 i 的最小质因子
else // p[j] 是 i 的最小质因子
{
phi[i * p[j]] = phi[i] * (p[j] - 1);
break;
}
}
}
}
3. 线性筛莫比乌斯函数
一句介绍:\(\mu(n) = \left\{\begin{matrix}1 & n = 1\\ 0 & n 含有平方因子\\ (-1)^{本质不同质因子个数} & \mathrm{otherwise} \end{matrix}\right.\)
一句求解:按照定义查找 \(n\) 的质因子个数。
现在讨论怎么在线性筛质数把莫比乌斯函数也求出来。
- 若 \(i\) 是质数,那么 \(i\) 就只有一个质因子 \(i\),所以 \(\mu(i) = (-1)^1 = -1\);
- 枚举 \(j\),找到 \(p_j \times i\),分两种情况求解 \(\mu(p_j \times i)\):
- 若 \(i \bmod p_j = 0\),也就是 \(p_j\) 是 \(i\) 的最小质因子。设 \(q = \dfrac i{p_j}\),那么显然有 \(p_j \times i = q \times {p_j}^2\),所以 \(p_j \times i\) 就有了一个平方因子,所以 \(\mu(p_j \times i) = 0\);
- 若 \(i \bmod p_j \ne 0\),也就是 \(p_j\) 不是 \(i\) 的最小质因子。那么 \(p_j \times i\) 就比 \(i\) 多了一个新的质因子 \(p_j\),故它的本质不同的质因子个数就多了 \(1\),所以 \(\mu(p_j \times i) = \mu(i) \times (-1) = -\mu(i)\)。也可以根据积性函数的定义,故 \(\mu(p_j \times i) = \mu(p_j) \times \mu(i) = \mu(i) \times (-1) = -\mu(i)\)。
int p[N]; // p[i] 表示第 i 个质数
bool st[N]; // st[i] 表示 i 是否是质数
int mu[N]; // mu[i] 表示 μ(i) 的值
int cnt;
void prime(int n)
{
mu[1] = 1;
for (int i = 2; i <= n; i ++ )
{
if (!st[i]) p[ ++ cnt] = i, mu[i] = -1;
for (int j = 1; p[j] <= n / i; j ++ )
{
st[p[j] * i] = 1;
if (i % p[j]) mu[p[j] * i] = -mu[i]; // p[j] 不是 i 的最小质因子
else // p[j] 是 i 的最小质因子
{
mu[i * p[j]] = 0;
break;
}
}
}
}
4. 线性筛约数个数
一句介绍:\(\sigma_0(n)\) 表示 \(n\) 的约数个数。
一句求解:若 \(n = \prod\limits_{i=1}^mp_i^{c_i}\),那么根据乘法原理,有 \(\sigma_0(n) = \prod\limits_{i=1}^m(c_i+1)\)。
现在讨论怎么在线性筛质数把约数个数也求出来。
在线性筛约数个数时还需要额外记录一个 \(num_i\) 数组,表示 \(i\) 的最小质因子的出现次数。
- 若 \(i\) 是质数,那么根据质数的定义,\(i\) 只有两个约数 \(1\) 和 \(i\),故 \(\sigma_0(i) = 2\),而且显然 \(num_i = 1\);
- 枚举 \(j\),找到 \(p_j \times i\),分两种情况求解 \(\sigma_0(p_j \times i)\):
- 若 \(i \bmod p_j = 0\),也就是 \(p_j\) 是 \(i\) 的最小质因子。那么 \(i\) 原来有 \(p_j\) 这个约数,并且是最小的约数,现在的 \(p_j \times i\) 相比 \(i\) 又多了一个 \(p_j\),也就是原来的 \(\times (p_j + 1)\) 变成了 \(\times (p_j + 1 + 1)\),也就是 \(\times (p_j + 2)\)。所以求 \(\sigma_0(p_j \times i)\) 时就需要先把原来 \((p_j + 1)\) 的贡献移除,在重新加上 \((p_j + 2)\) 的贡献,也就是 \(\sigma_0(p_j \times i) = \sigma_0(i) \times \dfrac{p_j+2}{p_j+1}\);
- 若 \(i \bmod p_j \ne 0\),也就是 \(p_j\) 不是 \(i\) 的最小质因子。那么也就是说原来的 \(i\) 中没有 \(p_j\) 这个约数,所以把 \(p_j \times i\) 分解质因数后应该有一项是 \(p_j^1\),剩下的是原来 \(i\) 分解质因数的结果。根据约数个数的公式,将所有的指数加 \(1\) 后连乘,那么就应该在原来 \(\sigma_0(i)\) 的基础上 \(\times (1+1)\),所以 \(\sigma_0(p_j \times i) = 2 \times \sigma_0(i)\)。也可以根据积性函数的定义,故 \(\sigma_0(p_j \times i) = \sigma_0(p_j) \times \sigma_0(i) = 2 \times \sigma_0(i)\)。
int p[N]; // p[i] 表示第 i 个质数
bool st[N]; // st[i] 表示 i 是否是质数
int d[N]; // d[i] 表示 i 的约数个数
int num[N]; // num[i] 表示 i 的最小质因子出现次数
int cnt;
void prime(int n)
{
d[1] = 1;
for (int i = 2; i <= n; i ++ )
{
if (!st[i]) p[ ++ cnt] = i, d[i] = 2, num[i] = 1;
for (int j = 1; p[j] <= n / i; j ++ )
{
st[p[j] * i] = 1;
if (i % p[j]) num[i * p[j]] = 1, d[i * p[j]] = d[i] * 2; // p[j] 不是 i 的最小质因子
else // p[j] 是 i 的最小质因子
{
num[i * p[j]] = num[i] + 1, d[i * p[j]] = d[i] / (num[i * p[j]] + 1) * (num[i * p[j]] + 2);
break;
}
}
}
}
5. 线性筛约数和
一句介绍:\(\sigma_1(n)\) 表示 \(n\) 的约数和。
一句求解:若 \(n = \prod\limits_{i=1}^mp_i^{c_i}\),有 \(\sigma_1(n) = \prod\limits_{i=1}^m\sum\limits_{j=0}^{c_i}p_i^j\)。
现在讨论怎么在线性筛质数把约数和也求出来。
在线性筛约数个数时也需要额外记录一个 \(num_i\) 数组,表示 \(i\) 的最小质因子的贡献和(若 \(i\) 的最小质因子是 \(p_1\),共出现了 \(c_1\) 次,那么 \(num_i = \sum\limits_{j=0}^{c_1}p_1^j\))。
- 若 \(i\) 是质数,那么根据质数的定义,\(i\) 只有两个约数 \(1\) 和 \(i\),故 \(\sigma_1(i) = 1 + i\),而且显然 \(num_i\) 也是 \(1 + i\);
- 枚举 \(j\),找到 \(p_j \times i\),分两种情况求解 \(\sigma_1(p_j \times i)\):
- 若 \(i \bmod p_j = 0\),也就是 \(p_j\) 是 \(i\) 的最小质因子。那么 \(i\) 原来有 \(p_j\) 这个约数,并且是最小的约数,所以 \(p_j = p_1\)。而现在的 \(p_j \times i\) 相比 \(i\) 又多了一个 \(p_j\),也就是原来的 \(\times \sum\limits_{k=0}^{c_1}p_j^k\) 变成了 \(\times \sum\limits_{k=0}^{c_1 + 1}p_j^k\)。所以求 \(\sigma_1(p_j \times i)\) 时就需要先把原来 \(\sum\limits_{k=0}^{c_1}p_j^k\) 的贡献移除,在重新加上 \(\sum\limits_{k=0}^{c_1 + 1}p_j^k\) 的贡献,而又有 \(\sum\limits_{k=0}^{c_1 + 1}p_j^k = \sum\limits_{k=0}^{c_1}p_j^k \times p_j + 1\),所以 \(\sigma_1(p_j \times i) = \sigma_1(i) \times \dfrac{num_i \times p_j + 1}{num_i}\);
- 若 \(i \bmod p_j \ne 0\),也就是 \(p_j\) 不是 \(i\) 的最小质因子。那么也就是说原来的 \(i\) 中没有 \(p_j\) 这个约数,所以把 \(p_j \times i\) 分解质因数后应该有一项是 \(p_j^1\),剩下的是原来 \(i\) 分解质因数的结果。根据约数和的公式,应该在原来 \(\sigma_1(i)\) 的基础上 \(\times (1+p_j^1)\),所以 \(\sigma_1(p_j \times i) = (p_j + 1) \times \sigma_1(i)\)。也可以根据积性函数的定义,故 \(\sigma_1(p_j \times i) = \sigma_1(p_j) \times \sigma_1(i) = (p_j + 1) \times \sigma_1(i)\)。
int p[N]; // p[i] 表示第 i 个质数
bool st[N]; // st[i] 表示 i 是否是质数
int d[N]; // d[i] 表示 i 的约数和
int num[N]; // num[i] 表示 i 的最小质因子的贡献和
void prime(int n)
{
d[1] = 1;
for (int i = 2; i <= n; i ++ )
{
if (!st[i]) p[ ++ cnt] = i, d[i] = num[i] = i + 1;
for (int j = 1; p[j] <= n / i; j ++ )
{
st[p[j] * i] = true;
if (i % p[j]) d[p[j] * i] = d[i] * (p[j] + 1); // p[j] 不是 i 的最小质因子
else // p[j] 是 i 的最小质因子
{
d[p[j] * i] = d[i] / num[i] * (num[i] * p[j] + 1);
num[p[j] * i] = num[i] * p[j] + 1;
break;
}
}
}
}
6. 总结及参考文献
在数论中,积性函数几乎无处不在。熟练掌握它们的求解过程乃重中之重。
参考文献:
- OI - Wiki : https://oi-wiki.org/math/number-theory/sieve/;
- 线性筛求约数个数以及约数和 : https://blog.csdn.net/calculate23/article/details/89513721;
- 一些积性函数 : https://blog.csdn.net/weixin_30244889/article/details/95661524;
- ChatGPT。
完。