线性筛法求积性函数
极难3线性筛法求积性函数——如何快速算出1到100万的函数值?
想象一下,你有一堆数字,要计算每个数的“欧拉函数”(就是比它小且与它互质的数的个数)。比如 φ(12)=4(因为1,5,7,11)。如果直接每个数分解质因数,算100万个数会非常慢(可能电脑要跑几秒钟甚至更久)。而线性筛法就像一条流水线,只用扫一遍就能同时筛出所有质数,并顺便算出每个数的积性函数值,速度极快。
1. 为什么需要线性筛?——从生活例子理解“避免重复劳动”
假设你要给全班40个同学发作业本,每个同学都有一份名单,要算出自己的“好朋友个数”(类似欧拉函数)。最笨的办法是:每个人挨个检查全班同学是不是好朋友,那样每个同学要检查40次,总共1600次。但如果你先按“身高”排好队(相当于筛质数),然后让个子最小的同学先告诉别人“我是你最小的朋友”,这样每个人只用和自己前面的人比一次,总共40次就够了。
核心思想:
- 每个合数(比如12=2×2×3)只会被它的最小质因子(2)筛掉一次。
- 在筛的过程中,我们能知道这个合数的质因子信息,从而用积性函数的递推公式快速得到函数值。
2. 关键概念——你需要记住的“三件法宝”
(1)积性函数的“分拆”性质
如果函数 f(n) 满足:当 a 和 b 互质时,f(a×b)=f(a)×f(b),那么就是积性函数。
比如欧拉函数 φ(n)、除数函数 d(n)(因子个数)、莫比乌斯函数 μ(n) 都是积性函数。
怎么用它?
对于任意数 n,把它写成最小质因子的幂次乘以剩下的互质部分:
n = p^e × m,其中 p 是 n 的最小质因子,且 p 不整除 m。
那么 f(n) = f(p^e) × f(m)。
所以只要知道 f(p^e)(质数幂上的值),就能算 f(n)。
(2)最小质因子的幂次——low 数组的作用
在线性筛中,我们需要记录每个数的最小质因子的幂次(即 p^e,其中 p 是最小质因子,e 是指数)。
用 low[n] 表示这个值。例如:
low[12]:12=2²×3,最小质因子2,指数2,所以low[12]=2²=4。low[18]=2¹×3²→最小质因子2,指数1,所以 low[18]=2。- 如果
n本身就是质数,比如n=7,则low[7]=7。
为什么需要 low?
因为在递推时,我们经常遇到两种情况:
- 如果当前数
i和质数p互质(p不是i的因子),那么i×p的最小质因子就是p,且low[i×p]=p。 - 如果
p是i的因子,那么i×p的最小质因子还是p,但指数增加了:low[i×p] = low[i] × p。
(3)两种递推情况——就像分水果
假设你已经知道了 f(i) 的值,现在要算 f(i×p)(p是质数):
情况1:p 不是 i 的因子(即 i % p != 0)
- 那么
p和i互质,i×p可以拆成i和p两部分,并且这两部分互质。 - 直接相乘:
f(i×p) = f(i) × f(p)。 - 同时
low[i×p] = p(因为p是新的最小质因子)。
情况2:p 是 i 的因子(即 i % p == 0)
- 此时
p已经是i的最小质因子,所以i×p的最小质因子还是p,但指数加1。 - 不能直接相乘,因为
p与i不互质。我们需要拆成最小质因子的幂次和互质部分:- 令
i = low[i] × rest,其中low[i]=p^e,rest与p互质。 - 那么
i×p = (low[i]×p) × rest = low[i×p] × rest。
- 令
- 根据积性:
f(i×p) = f(low[i×p]) × f(rest)。 - 而
f(rest) = f(i) / f(low[i])(因为积性,且 rest 与 low[i] 互质)。 - 所以公式可以写成:
f(i×p) = f(low[i]×p) × f(i) / f(low[i])。
简化技巧:
对于欧拉函数,有一个特别简单的递推公式:
- 如果
p是i的最小质因子,那么φ(i×p) = p × φ(i)。
这个公式的推导在后面的欧拉函数例子中会详细解释。
3. 以欧拉函数为例——最常用的积性函数
欧拉函数的性质(复习)
- 若
p是质数:φ(p) = p-1。 - 若
n = p^e:φ(p^e) = p^e - p^{e-1} = p^{e-1} × (p-1)。 - 积性:若
gcd(a,b)=1,则φ(ab)=φ(a)φ(b)。
为什么当 p 是 i 的因子时 φ(i×p)=p×φ(i)?
设 i = p^e × m(m与p互质),则 i×p = p^{e+1} × m。
由于 φ 在质数幂上有公式:φ(p^{e+1}) = p^{e+1} - p^e = p × (p^e - p^{e-1}) = p × φ(p^e)。
而 φ 是积性函数,且 p^{e+1} 与 m 互质,所以:
φ(i×p) = φ(p^{e+1}) × φ(m) = [p × φ(p^e)] × φ(m) = p × [φ(p^e) × φ(m)] = p × φ(i)。
完美!
用生活例子理解这个递推
想象你有两种颜色的球:红色球(代表质数因子)和蓝色球(代表其他因子)。如果新加进来的红色球和已有的红色球颜色相同(即p是i的因子),那么红色球的数量会多一个,但它们的“贡献”是乘以红色球的个数(因为欧拉函数对相同质因子的处理是乘法)。如果新加的是新颜色(即p与i互质),那么两种颜色独立,直接相乘。
4. 新手容易犯的 5 个错误
-
忘记初始化
phi[1]和low[1]
虽然1是特殊的,但很多积性函数定义f(1)=1。如果不初始化,后面的计算会出错。 -
把
break放错位置
在情况2(i % p == 0)中,处理完i×p后必须立即break,因为对于更大的质数,i×p的最小质因子已经不是它们了(否则会重复计算)。如果忘记break,后面会错误地更新同一个合数多次。 -
混淆
low[x] == x的条件
“low[x] == x” 表示x是某个质数的幂(即x = p^e)。但这个判断的前提是i % p == 0且我们已经更新了low[x] = low[i] * p。只有当i本身也是质数幂时,low[x]==x才成立。如果i有其他质因子,那么low[x]只是最小质因子的幂次,小于x。 -
运算溢出
比如phi[i] * p可能超过int范围,应该使用long long。 -
不理解为什么情况2中直接
phi[x] = phi[i] * p就是正确的
很多新手会想:如果i不是质数幂(比如i=12,含有因子2²×3),那么i×p的最小质因子幂次low[12×2] = 2³ = 8,但12×2=24还有因子3,所以low[24]=8,而phi[24]不能简单等于phi[12] * 2。实际上,上一节我们已经证明了只有当low[x]==x(即x是纯质数幂)时才能直接用phi[i] * p。对于不是纯质数幂的情况,我们需要拆成low和剩余部分。代码已经通过if (low[x]==x)分支处理了,所以没问题。
5. 完整可运行的代码示例(C++ 和 Python)
C++ 实现(带详细注释)
#include <iostream>
#include <vector>
using namespace std;
const int MAXN = 1000000;
long long phi[MAXN + 1]; // 欧拉函数值
int low[MAXN + 1]; // 最小质因子的幂次,即 p^e
vector<int> primes; // 质数列表
bool is_composite[MAXN + 1] = {false}; // 标记是否为合数
void linear_sieve_phi(int n) {
phi[1] = 1; // φ(1)=1
low[1] = 1; // low[1]=1
for (int i = 2; i <= n; i++) {
if (!is_composite[i]) {
primes.push_back(i);
// i 是质数
phi[i] = i - 1; // 质数的欧拉函数 = p-1
low[i] = i; // 最小质因子的幂次就是它本身 (p^1)
}
// 枚举所有小于等于i的最小质因子的质数
for (size_t j = 0; j < primes.size() && i * primes[j] <= n; j++) {
int p = primes[j];
int x = i * p; // 我们要计算的合数
is_composite[x] = true; // 标记为合数
if (i % p == 0) {
// 情况2:p是i的最小质因子(因为p <= i的最小质因子)
low[x] = low[i] * p; // 更新最小质因子的幂次
if (low[x] == x) {
// 如果low[x]==x,说明x是质数幂,比如 p^{e+1}
// 那么可以用递推公式:φ(p^{e+1}) = p * φ(p^e)
phi[x] = phi[i] * p;
} else {
// 否则,x不是纯质数幂,需要拆成 low[x] 和 x/low[x] 两部分
// 因为 low[x] 与 x/low[x] 互质
phi[x] = phi[low[x]] * phi[x / low[x]];
}
break; // 注意:必须break,因为更大的质数p'会使得i*p'的最小质因子不是p'
} else {
// 情况1:p与i互质,p是x的最小质因子
low[x] = p;
phi[x] = phi[i] * phi[p]; // 积性直接相乘
}
}
}
}
int main() {
int N;
cout << "请输入 N (建议不超过 1000000): ";
cin >> N;
if (N > MAXN) {
cerr << "N 太大,请修改 MAXN" << endl;
return 1;
}
linear_sieve_phi(N);
cout << "前10个欧拉函数值:" << endl;
for (int i = 1; i <= min(N, 10); i++) {
cout << "φ(" << i << ") = " << phi[i] << endl;
}
return 0;
}
Python 实现(带详细注释)
MAXN = 1000000
phi = [0] * (MAXN + 1) # 欧拉函数值列表
low = [0] * (MAXN + 1) # 最小质因子幂次列表
primes = [] # 质数列表
is_composite = [False] * (MAXN + 1) # 合数标记
def linear_sieve_phi(n):
phi[1] = 1
low[1] = 1
for i in range(2, n + 1):
if not is_composite[i]:
primes.append(i)
phi[i] = i - 1
low[i] = i # 质数的最小质因子幂次就是本身
# 枚举质数
for p in primes:
if i * p > n:
break
x = i * p
is_composite[x] = True
if i % p == 0:
# 情况2:p是i的最小质因子
low[x] = low[i] * p
if low[x] == x:
# x是质数幂
phi[x] = phi[i] * p
else:
# x不是纯质数幂,拆开计算
phi[x] = phi[low[x]] * phi[x // low[x]]
break # 重要:必须break
else:
# 情况1:p与i互质
low[x] = p
phi[x] = phi[i] * phi[p]
# 使用示例
N = int(input("请输入 N (建议不超过 1000000): "))
linear_sieve_phi(N)
print("前10个欧拉函数值:")
for i in range(1, min(N, 10) + 1):
print(f"φ({i}) = {phi[i]}")
6. 推广到任意积性函数——只要知道“质数幂上的公式”就行
你可能会问:如果我要算的不是欧拉函数,而是其他积性函数,比如除数函数 d(n)(因子个数)或莫比乌斯函数 μ(n),该怎么办?
核心思想不变:
- 对于质数幂
p^e,你需要知道f(p^e)的公式(或递推关系)。 - 在线性筛的两种情况下,依然可以用同样的分拆方法:
- 情况1(互质):
f(i×p) = f(i) × f(p) - 情况2(p是i的因子):
f(i×p) = f(low[i]×p) × f(i) / f(low[i])
或者更简单:如果f在质数幂上有递推公式,可以直接用f(i×p) = f(i) * 某个倍数(比如欧拉函数就是乘以 p)。
- 情况1(互质):
例子1:除数函数 d(n)
d(p^e) = e+1(因为因子有 p^0, p^1, ..., p^e 共 e+1 个)。- 在筛法中,情况2(p是i的因子)时,我们需要知道
d(low[i]*p),这可以通过记录指数来实现。
具体做法:除了low数组,还可以维护一个cnt数组表示最小质因子的指数e。
那么d(i×p)的计算:如果p是i的因子,则cnt[i×p] = cnt[i] + 1,然后d(i×p) = d(i) / (cnt[i]+1) * (cnt[i×p]+1)。
如果互质,则d(i×p) = d(i) * d(p) = d(i) * 2。
例子2:莫比乌斯函数 μ(n)
μ(1) = 1- 对于质数
p:μ(p) = -1 - 对于质数幂
p^e (e>=2):μ(p^e) = 0 - 积性性质:若
a和b互质,则μ(ab) = μ(a) μ(b)。
在线性筛中:
- 情况1(互质):
μ(i×p) = μ(i) * (-1) = -μ(i) - 情况2(p是i的因子):此时
i×p含有至少 p^2,所以μ(i×p) = 0(因为有平方因子)。
这样,用同样的框架,只需要修改 phi 部分为对应的函数名和规则即可。
7. 小结与练习
线性筛法求积性函数是数论编程的“神兵利器”,它能在 O(N) 的时间内算出所有 1~N 的函数值,是很多算法(比如莫比乌斯反演、杜教筛)的基础。理解它的关键是:
- 每个合数只被最小质因子访问一次。
- 利用
low数组记录最小质因子幂次,实现递推。
练习题目:
- 用线性筛法求 1 到 N 的除数函数
d(n)(因子个数)的值。 - 用线性筛法求 1 到 N 的莫比乌斯函数
μ(n)的值。
相关知识点指引:
- 如果你对欧拉函数还不熟悉,可以回顾“欧拉函数及其性质”。
- 学习完线性筛后,可以继续学习“莫比乌斯反演”,它大量使用线性筛预处理。
- 如果想了解更高效的求积性函数方法,可以看看“杜教筛”。
现在,打开你的编程环境,亲手实现一下线性筛求欧拉函数,再试试求除数函数吧!
例题精讲
线性筛法(欧拉筛)与普通埃氏筛的最大区别在于:
利用线性筛法可以批量计算任意积性函数f在1到n上的值,只需预先定义f(1)并给出f(p^k)与f(p^{k-1})的递推关系即可。
下面是用线性筛法求欧拉函数φ(n)的代码片段(1≤n≤N),请补全空缺部分。
int phi[N], primes[N], cnt;
bool is_composite[N];
void linear_sieve(int n) {
phi[1] = 1;
for (int i = 2; i <= n; ++i) {
if (!is_composite[i]) {
primes[cnt++] = i;
phi[i] = i - 1;
}
for (int j = 0; j < cnt && i * primes[j] <= n; ++j) {
is_composite[i * primes[j]] = true;
if (i % primes[j] == 0) {
phi[i * primes[j]] = phi[i] * primes[j];
___;
} else {
phi[i * primes[j]] = phi[i] * (primes[j] - 1);
}
}
}
}在用线性筛法求莫比乌斯函数μ(n)时,对于质数p,μ(p)的正确初始值是什么?
以下是用线性筛法求约数个数d(n)(n的正因子个数)的代码片段,已知d(1)=1,且若n = i * p(p为质数),记min_pow[i]表示i的最小质因子的幂次。请补全空缺处,使得当i % p == 0时能正确递推。
int d[N], min_pow[N];
void sieve(int n) {
d[1] = 1;
for (int i = 2; i <= n; ++i) {
if (!is_composite[i]) {
primes[cnt++] = i;
d[i] = 2;
min_pow[i] = 1; // 质数的最小质因子幂次为1
}
for (int j = 0; j < cnt && i * primes[j] <= n; ++j) {
int p = primes[j];
is_composite[i * p] = true;
if (i % p == 0) {
min_pow[i * p] = min_pow[i] + 1;
d[i * p] = d[i] / (min_pow[i] + 1) * (min_pow[i * p] + 1);
___;
} else {
min_pow[i * p] = 1;
d[i * p] = d[i] * d[p];
}
}
}
}