欧拉函数
欧拉函数 \(\phi(n)\) 就是:小于或等于 \(n\) 的正整数中,与 \(n\) 互质的数的个数。
当 \(n=7\) 时(一个质数),\(1,2,3,4,5,6\) 都和 \(7\) 互质,所以 \(\phi(7)=6\)。显然,如果 \(p\) 是一个质数,那么 \(\phi(p)=p-1\)。
当 \(n=8\) 时,互质的有 \(1,3,5,7\) 共 \(4\) 个,所以 \(\phi(8)=4\)。
要计算欧拉函数的值,如果一个一个枚举太慢了,有更快的计算公式。
这个公式的核心思想是排除法。从总数 \(n\) 开始,想办法排除掉那些与 \(n\) 共享质因子的数。
以 \(n=12\) 为例:
第一步:对 \(n\) 进行质因数分解
\(12 = 2^2 \times 3\),\(12\) 的质因数是 \(2\) 和 \(3\)。那么,任何一个数,只要它是 \(2\) 的倍数,或者 \(3\) 的倍数,它就和 \(12\) 共享质因子(除了 \(1\) 这个特殊情况)。
第二步:使用排除法
总共有 \(12\) 个数。要排除掉所有 \(2\) 的倍数。有多少个?\(\dfrac{12}{2} = 6\) 个。还要排除掉所有 \(3\) 的倍数。有多少个?\(\dfrac{12}{3} = 4\) 个。但是,像 \(6,12\) 这样的数,既是 \(2\) 的倍数又是 \(3\) 的倍数,被排除了两次。这就要用到容斥原理。
更巧妙的方法是这样想:在 \(1\) 到 \(12\) 中,不是 \(2\) 的倍数的数占了 \(1 - \dfrac{1}{2}\) 的比例。在剩下的数中,再排除掉 \(3\) 的倍数,相当于又乘以 \(1 - \dfrac{1}{3}\) 的比例。
第三步:得到欧拉函数的计算公式
\(\phi(12) = 12 \times \left( 1-\dfrac{1}{2} \right) \times \left( 1-\dfrac{1}{3} \right) = 12 \times \dfrac{1}{2} \times \dfrac{2}{3} = 4\)。而 \(1\) 到 \(12\) 中和 \(12\) 互质的有 \(1,5,7,11\) 正好 \(4\) 个数。
通用公式:如果 \(n\) 的质因数分解是 \(n = p_1^{k_1} \times p_2^{k_2} \times \cdots \times p_r^{k_r}\),其中 \(p_1, p_2, \dots\) 是不同的质因数,那么 \(\phi(n) = n \times \left( 1 - \dfrac{1}{p_1} \right) \times \left( 1 - \dfrac{1}{p_2} \right) \times \cdots \times \left( 1 - \dfrac{1}{p_r} \right)\)。
ll phi(ll n) {
ll res = n; // 初始化结果为n
// 遍历从2到sqrt(n)的所有可能因子
for (ll i = 2; i * i <= n; i++) {
if (n % i == 0) { // 如果i是n的一个质因数
// 应用公式:res = res * (1 - 1/i)
// 等价于 res = res - res / i,可以避免浮点数运算
res -= res / i;
// 把n中的所有因子i都除掉,确保每个质因数只使用一次
while (n % i == 0) {
n /= i;
}
}
}
if (n > 1) {
res -= res / n;
}
return res;
}
求一个数的欧拉函数值的时间复杂度为 \(O(\sqrt{n})\)。
如果要计算从 \(1\) 到 \(n\) 所有数的欧拉函数值,而每次都用上面的方法计算,会产生很多重复的质因数分解过程,效率不高,而使用筛法能批量去求值。
标准的埃氏筛是用来找质数的,它的思想是:如果一个数是一个质数,那么它的所有更大的倍数肯定都不是质数,把它们都划掉。
计算欧拉函数也可以借鉴这个思想,但要稍微改造一下:如果有一个质数 \(p\),那么对于所有更大的倍数 \(j\),它们的 \(\phi(j)\) 值都要受到影响。因为 \(p\) 是 \(j\) 的一个质因数,所以要去更新 \(\phi(j)\) 的计算。
以计算 \(1\) 到 \(12\) 的 \(\phi\) 值为例:
首先初始化 \(\phi_i = i\)。
从 \(i=2\) 开始遍历:
- 当 \(i = 2\) 时:
- 此时 \(\phi_2 = 2\),说明它没被前面的数动过,所以 \(2\) 是一个质数。
- 找到 \(2\) 的所有倍数 \(4,6,8,10,12\),并用 \(2\) 这个质因数去更新它们的 \(\phi\) 值。
- 对每个倍数 \(j\) 执行 \(\phi_j \leftarrow \phi_j \times \left( 1 - \dfrac{1}{2} \right)\)。
- \(\phi_2 = 2 \times \dfrac{1}{2} = 1\)(质数的欧拉函数值是自身减一)
- \(\phi_4 = 4 \times \dfrac{1}{2} = 2\)
- \(\phi_6 = 6 \times \dfrac{1}{2} = 3\)
- \(\phi_8 = 8 \times \dfrac{1}{2} = 4\)
- \(\phi_{10} = 10 \times \dfrac{1}{2} = 5\)
- \(\phi_{12} = 12 \times \dfrac{1}{2} = 6\)
- 目前 \(\phi = [1, \mathbf{1}, 3, \mathbf{2}, 5, \mathbf{3}, 7, \mathbf{4}, 9, \mathbf{5}, 11, \mathbf{6}]\)
- 当 \(i = 3\) 时:
- 此时 \(\phi_3 = 3\),没被动过,所以 \(3\) 是一个质数。
- 找到 \(3\) 的所有倍数 \(6,9,12\),用 \(3\) 去更新它们的 \(\phi\) 值。
- \(\phi_3 = 3 \times \left( 1 - \dfrac{1}{3} \right) = 2\)
- \(\phi_6 = 3 \times \left( 1 - \dfrac{1}{3} \right) = 2\)(在之前的基础上更新)
- \(\phi_9 = 9 \times \left( 1 - \dfrac{1}{3} \right) = 6\)
- \(\phi_{12} = 6 \times \left( 1 - \dfrac{1}{3} \right) = 4\)(在之前的基础上更新)
- 目前 \(\phi = [1, 1, \mathbf{2}, 2, 5, \mathbf{2}, 7, 4, \mathbf{6}, 5, 11, \mathbf{4}]\)
- 当 \(i = 4\) 时:
- 检查 \(\phi_4\),发现它变成了 \(2\),不再是 \(4\) 了。这说明 \(4\) 已经被前面的质数(\(2\))处理过了,它不是质数。直接跳过。
- 当 \(i = 5\) 时:
- 检查 \(\phi_5\),是 \(5\),是质数。
- 更新 \(5\) 的倍数 \(10\)。
- \(\phi_5 = 5 \times \left( 1 - \dfrac{1}{5} \right) = 4\)
- \(\phi_{10} = 5 \times \left( 1 - \dfrac{1}{5} \right) = 4\)
- 目前 \(\phi = [1, 1, 2, 2, \mathbf{4}, 2, 7, 4, 6, \mathbf{4}, 11, 4]\)
- 以此类推,直到遍历完所有数字。最终就能计算得到所有数字的欧拉函数值。
时间复杂度为 \(O(n \log \log n)\)。
void eulerPhiSieve(int n) {
for (int i = 1; i <= n; i++) phi[i] = i; // 初始化
for (int i = 2; i <= n; i++) {
if (phi[i] == i) { // i 是一个质数
for (int j = i; j <= n; j += i) { // 遍历 i 的所有倍数 j
// 应用公式 phi[j] = phi[j] * (1 - 1/i),避免浮点数运算
phi[j] = phi[j] / i * (i - 1);
}
}
}
}
线性筛的过程同样可以计算欧拉函数值。埃氏筛是用每个质数 \(p\) 去更新 \(p\) 的所有倍数。线性筛是用每个数 \(i\)(无论是不是质数),去乘以已找到的质数 \(p\),来得到一个合数 \(i \times p\),并计算 \(\phi(i \times p)\)。
在计算 \(\phi(i \times p)\) 时(其中 \(p\) 是一个质数),有两种情况:
-
\(p\) 是 \(i\) 的最小质因数
i % p == 0- 这意味着 \(i\) 和 \(i \times p\) 拥有完全相同的质因数集合。
- 例如,\(i=6\)(质因数 \(2,3\)),\(p=2\),\(i \times p = 12\)(质因数还是 \(2,3\))。
- 根据欧拉函数公式 \(\phi(n) = n \times \left( 1 - \dfrac{1}{p_1} \right) \times \cdots\),因为质因数集合一样,所以后面那一长串的值是相同的。
- 所以 \(\phi(i \times p) = (i \times p) \times 公共部分 = (i \times 公共部分) \times p = \phi(i) \times p\)。
phi[i * p] = phi[i] * p
-
\(p\) 比 \(i\) 的所有质因数都小
i % p != 0- 这意味着 \(p\) 是 \(i \times p\) 的最小质因数,并且 \(p\) 和 \(i\) 互质。
- 例如 \(i=5, p=2\),\(i \times p = 10\)。\(p=2\) 是 \(10\) 的最小质因数。
- 欧拉函数是一个积性函数,意思是如果 \(a\) 和 \(b\) 互质,则 \(\phi(a \times b) = \phi(a) \times \phi(b)\)。
- 因为 \(p\) 和 \(i\) 互质,所以 \(\phi(i \times p) = \phi(i) \times \phi(p)\)。
- 又因为 \(p\) 是质数,\(\phi(p) = p - 1\)。
phi[i * p] = phi[i] * (p - 1)
void eulerPhiLinearSieve(int n) {
for (int i = 2; i <= n; i++) is_prime[i] = true;
phi[1] = 1; is_prime[0] = is_prime[1] = false; // 初始化
for (int i = 2; i <= n; i++) {
if (is_prime[i]) {
primes.push_back(i);
phi[i] = i - 1;
}
for (int p : primes) {
if (1ll * i * p > n) { // 如果 i*p 超出范围,停止
break;
}
is_prime[i * p] = false; // 标记 i*p 为合数
if (i % p == 0) { // p 是 i 的最小质因数
phi[i * p] = phi[i] * p;
break; // 保证每个合数只被其最小质因数筛一次
} else { // p 比 i 的所有质因数都小
phi[i * p] = phi[i] * (p - 1);
}
}
}
}
时间复杂度为 \(O(n)\)。
习题:P2158 [SDOI2008] 仪仗队
解题思路
考虑一条斜线在横向和竖向上的长度。
按横向长度分类,当竖向长度小于等于横向长度时,竖向长度必须跟横向长度互质,并且可以交换横向和竖向长度。
因此当横向长度大于等于 \(2\) 时,就有 \(2 \times (\phi_2 + \cdots + \phi_{n-1})\) 条斜线。
另外当 \(n \ge 2\) 时,会有 \((0,1), (1, 0), (1,1)\) 这三条线还没算进来。因此当 \(n \ge 2\) 时,答案再加 \(3\)。
参考代码
#include <cstdio>
#include <vector>
using ll = long long;
using std::vector;
const int N = 40005;
bool is_prime[N];
vector<int> primes;
int phi[N];
void sieve(int n) {
for (int i = 2; i <= n; i++) is_prime[i] = true;
phi[1] = 1; is_prime[0] = is_prime[1] = false;
for (int i = 2; i <= n; i++) {
if (is_prime[i]) {
primes.push_back(i);
phi[i] = i - 1;
}
for (int p : primes) {
if (1ll * i * p > n) break;
is_prime[i * p] = false;
if (i % p == 0) {
phi[i * p] = phi[i] * p;
break;
} else {
phi[i * p] = phi[i] * (p - 1);
}
}
}
}
int main()
{
int n; scanf("%d", &n);
sieve(n);
ll ans = 0;
for (int i = 2; i < n; i++) {
ans += phi[i] * 2;
}
if (n > 1) ans += 3;
printf("%lld\n", ans);
return 0;
}
习题:P3601 签到题
解题思路
显然 \(qiandao(x) = x - \phi(x)\)。问题的核心在于如何高效地计算区间 \([l,r]\) 内每个数的欧拉函数 \(\phi(i)\)。
\(r - l\) 的范围相对较小,最大为 \(10^6\)。这个特点说明应该使用一种与区间长度相关的算法。
先用筛法预处理出 \(\sqrt{r} = 10^6\) 以内的所有质数,用埃氏筛的思想对这些质数在区间内的倍数更新欧拉函数值。
如果一个数包含大于 \(10^6\) 的质因数,那么这样的因子最多只有一个,可以在筛法过程中将因子不断除掉,最后如果剩余的数大于 \(1\) 再更新一次欧拉函数。
参考代码
#include <cstdio>
#include <vector>
// 使用 long long 别名,因为 l, r 可能很大
using ll = long long;
// 使用 std::vector
using std::vector;
// 定义常量
const int N = 1000000;
const int MOD = 666623333;
// isprime: 埃氏筛用的布尔数组,isprime[i]为true表示i是质数
bool isprime[N + 5];
// prime: 存储预处理出的所有小于等于 N 的质数
vector<int> prime;
// phi: 存储区间 [l, r] 中每个数的欧拉函数值。phi[i] 对应 l+i 的欧拉函数
// num: 存储区间 [l, r] 中每个数本身,用于分解质因数。num[i] 对应 l+i
ll phi[N + 5], num[N + 5];
int main()
{
// 读入区间左右端点
ll l, r; scanf("%lld%lld", &l, &r);
// 特殊处理 l=1 的情况。qiandao(1) = 1 - φ(1) = 0。
// 为了方便计算,直接从 2 开始。
if (l == 1) l = 2;
// 数组下标 i 对应的值是 l+i。
// phi[i] 初始化为 l+i,作为计算欧拉函数的初始值。
// num[i] 也初始化为 l+i,用于后续的质因数分解。
for (int i = 0; i <= r - l; i++) {
phi[i] = num[i] = l + i;
}
// 初始化所有数(从2开始)为质数
for (int i = 2; i <= N; i++) isprime[i] = true;
// 筛掉合数
for (int i = 2; i * i <= N; i++) {
if (isprime[i]) {
for (int j = i * i; j <= N; j += i) {
isprime[j] = false;
}
}
}
// 将所有质数存入 vector
for (int i = 2; i <= N; i++) {
if (isprime[i]) prime.push_back(i);
}
// 遍历所有小于等于 10^6 的质数 p
for (int p : prime) {
// 找到区间 [l, r] 中 p 的第一个倍数 start
// (l + p - 1) / p 是向上取整的 ceil(l/p)
ll start = (l + p - 1) / p * p;
// 从 start 开始,遍历区间内所有 p 的倍数
for (ll i = (l + p - 1) / p * p; i <= r; i += p) {
// 根据公式 φ(n) = n * Π(1 - 1/p),更新 phi 值
// phi[i-l] = phi[i-l] / p * (p-1)
phi[i - l] = phi[i - l] / p * (p - 1);
// 将 i 的质因子 p 除尽
while (num[i - l] % p == 0) num[i - l] /= p;
}
}
ll ans = 0;
for (ll i = l; i <= r; i++) {
// 如果 num[i-l] > 1,说明 i 有一个大于 10^6 的大质因数
// 这个大质因数就是 num[i-l] 本身
if (num[i - l] > 1) phi[i - l] = phi[i - l] / num[i - l] * (num[i - l] - 1);
// 累加 qiandao(i) = i - φ(i)
ans += (i - phi[i - l]);
// 每一步都取模,防止溢出
ans %= MOD;
}
// 输出最终答案
printf("%lld\n", ans);
return 0;
}
习题:P2568 GCD
解题思路
可以枚举每一个素数 \(p\),然后计算有多少数对 \((x,y)\) 满足 \(\gcd(x,y) = p\)。将所有素数对应的结果累加起来就是最终答案。
如果 \(\gcd(x,y)=p\),那么 \(x,y\) 都必须是 \(p\) 的倍数。可以设 \(x = i \cdot p\),\(y = j \cdot p\),根据 \(\gcd\) 的性质,\(\gcd(i \cdot p, j \cdot p) = p \cdot \gcd(i,j)\)。所以,原条件 \(\gcd(x,y)=p\) 就转化为了 \(p \cdot \gcd(i,j) = p\),即 \(\gcd(i,j)=1\)。
因为 \(1 \le x,y \le n\),所以 \(1 \le i \cdot p \le n\) 且 \(1 \le j \cdot p \le n\)。这可以导出 \(1 \le i \le \left\lfloor \dfrac{n}{p} \right\rfloor\) 且 \(1 \le j \le \left\lfloor \dfrac{n}{p} \right\rfloor\)。
现在问题变成了:对于一个不大于 \(n\) 的素数 \(p\),需要求出满足 \(1 \le i,j \le \left\lfloor \dfrac{n}{p} \right\rfloor\) 且 \(\gcd(i,j)=1\) 的数对 \((i,j)\) 的数量。
计算在 \(1 \le i,j \le m\)(这里 \(m = \left\lfloor \dfrac{n}{p} \right\rfloor\))范围内互质的数对 \((i,j)\) 的数量,可以利用欧拉函数 \(\phi\)。\(\phi(k)\) 表示小于等于 \(k\) 且与 \(k\) 互质的正整数的个数。当 \(i=j\) 时,要使 \(\gcd(i,i)=1\),只有 \(i=1\) 一种情况,即数对 \((1,1)\)。当 \(i \ne j\) 时,可以只考虑 \(i \lt j\) 的情况,其数量为 \(\sum \limits_{k=2}^m \phi(k)\)。由于对称性(\(\gcd(i,j)=\gcd(j,i)\)),\(j \lt i\) 的情况数量也是这么多。所以总数为 \(1 + 2 \times \sum \limits_{k=2}^m \phi(k)\)。因为 \(\phi(1)=1\),所以这个式子可以写成 \(1 + 2 \times (\sum \limits_{k=1}^m \phi(k) - \phi(1)) = 1 + 2 \times (\sum \limits_{k=1}^m \phi(k) - 1) = 2 \times \sum \limits_{k=1}^m \phi(k) - 1\)。
算法实现:
- 使用线性筛(欧拉筛)在 \(O(n)\) 的时间内预处理 \(1\) 到 \(n\) 的所有素数以及每个数的欧拉函数 \(\phi\) 值。
- 对 \(\phi\) 数组计算前缀和,这样就可以在 \(O(1)\) 的时间内得到 \(\sum \limits_{k=1}^m \phi(k)\)。
- 遍历所有预处理出的素数 \(p\),对于每个 \(p\),计算出 \(m = \left \lfloor \dfrac{n}{p} \right \rfloor\),然后通过前缀和数组查到对应的互质数对数量,并累加到最终答案中。
参考代码
#include <cstdio>
#include <vector>
using ll = long long;
using std::vector;
const int N = 10000005; // n 的最大值为 10^7
ll phi[N]; // phi[i] 存储欧拉函数 phi(i) 的值,后期用于存储前缀和
vector<int> primes; // 存储所有筛出来的素数
bool is_prime[N]; // is_prime[i] 标记 i 是否为素数
// 线性筛(欧拉筛),用于在 O(n) 时间内预处理素数和欧拉函数
void sieve(int n) {
// 初始化,假设 2~n 都是素数
for (int i = 2; i <= n; i++) is_prime[i] = true;
phi[1] = 1; // phi(1) 定义为 1
is_prime[0] = is_prime[1] = false; // 0 和 1 不是素数
for (int i = 2; i <= n; i++) {
if (is_prime[i]) { // 如果 i 是素数
primes.push_back(i); // 将 i 加入素数列表
phi[i] = i - 1; // 素数 p 的 phi 值为 p-1
}
// 遍历已找到的素数 p,用来筛掉合数
for (int p : primes) {
// 如果 i*p 超出范围,则更大的素数也会超,直接退出
if (1ll * i * p > n) break;
is_prime[i * p] = false; // 将 i*p 标记为合数
if (i % p == 0) { // 如果 p 是 i 的一个质因子
// 根据欧拉函数性质:若 p|i, 则 phi(i*p) = phi(i) * p
phi[i * p] = phi[i] * p;
// 找到 i 的最小质因子 p,就退出循环
// 这是线性筛的关键,保证每个合数只被其最小质因子筛掉一次
break;
} else { // 如果 p 不是 i 的质因子 (即 gcd(i, p) = 1)
// 根据欧拉函数性质(积性函数):若 gcd(i,p)=1, 则 phi(i*p) = phi(i) * phi(p) = phi(i) * (p-1)
phi[i * p] = phi[i] * (p - 1);
}
}
}
}
int main()
{
int n;
scanf("%d", &n);
// 1. 预处理
sieve(n);
// 2. 计算 phi 函数的前缀和
// 执行后,phi[i] 的含义变为 sum_{k=1 to i} phi(k)
for (int i = 1; i <= n; i++) phi[i] += phi[i - 1];
ll ans = 0;
// 3. 遍历所有素数 p
for (int p : primes) {
// 对于每个素数 p,其贡献的数对数量为 (2 * sum_{k=1 to n/p} phi(k)) - 1
// phi[n / p] 就是我们预处理好的 phi 函数前缀和
ans += phi[n / p] * 2 - 1;
}
printf("%lld\n", ans);
return 0;
}
习题:P2303 [SDOI2012] Longge 的问题
解题思路
数论求和问题通常的优化思路是改变求和对象。与其枚举 \(i\),不如枚举 \(\gcd(i,n)\) 的可能值。
一个重要的性质是:\(\gcd(i,n)\) 的结果必然是 \(n\) 的一个约数。设 \(d\) 是 \(n\) 的一个约数,可以统计有多少个 \(i\)(在 \(1\) 到 \(n\) 之间)满足 \(\gcd(i,n)=d\),然后把它们的贡献 \(d\) 统一计算。
这样,原式就可以改写为:\(\sum_{d|n} \sum_{\substack{1 \le i \le n \\ \gcd(i, n) = d}} d\),其中 \(d|n\) 表示 \(d\) 是 \(n\) 的约数。这个式子可以进一步简化为:\(\sum_{d|n} d \cdot (\text{满足 } 1 \le i \le n \text{ 且 } \gcd(i, n) = d \text{ 的 } i \text{ 的个数})\)。
后面那一项实际上就是欧拉函数 \(\phi(n/d)\),最终计算的公式:\(\sum_{d|n} d \cdot \phi(n/d)\)。
总的时间复杂度的粗略上界可以认为是 \(O(d(n) \sqrt{n})\)。其中 \(d(n)\) 表示数据范围内最多的约数个数,在本题中大约为一千多。
参考代码
#include <cstdio>
using ll = long long;
ll calc(ll n) {
ll phi = n, i = 2;
while (i * i <= n) {
if (n % i == 0) {
phi = phi / i * (i - 1);
while (n % i == 0) n /= i;
}
i++;
}
if (n > 1) phi = phi / n * (n - 1);
return phi;
}
int main()
{
ll n; scanf("%lld", &n);
ll ans = 0;
for (ll i = 1; i * i <= n; i++) {
if (n % i == 0) {
ans += i * calc(n / i);
if (i * i != n) ans += (n / i) * calc(i);
}
}
printf("%lld\n", ans);
return 0;
}

浙公网安备 33010602011771号