欧拉筛和筛法求欧拉函数——杨子曰数学
超链接:数学合集
我们都知道,再这个世界上,有一个筛质数算法叫做埃筛,它的代码长这样:
void get_prime(int n){
memset(flag,1,sizeof(flag));//flag开成bool就可以用memset赋值成1了
flag[1]=0;
for (int i=2;i<=n;i++){
if (!flag[i]) continue;
pr[++cnt]=i;
for (int j=2;i*j<=n;j++){
flag[i*j]=0;
}
}
}
但是这种算法的太慢了(其实还好啦!),只有O(n log log n),不能满足我们的需求,今天给大家曰一种特别nb的筛素数的算法——欧拉筛,复杂度是O(n),优不优秀!!(你可能会说:也没比埃筛快多少呀!,在下面你会被它的用途,好处惊到)
先来思考一下,埃筛为什么会慢?因为它会把一个合数重复筛很多遍,比如12会被2筛一遍,会被3筛一遍,那么我们有没有什么办法让一个数只被筛一次捏?黑喂狗:
我们要做到的是一个合数只被它最小的质因数筛一次,So,我们需要用一个数组v[i],表示i的最小质因数,那么也就是说如果v[i]=i则说明i是一个质数,现在我们要想办法维护这个数组v
我们这样来操作:
- 把v数组全部赋成0
- 从2~n考虑每一个i
- 如果扫到当前v[i]=0,也就是还没有被比它小的任何一个质数更新到,说明这是一个质数,记录下来
- 对于i,我们扫描每个小于v[i]的质数p,那么对于i*p这个数,它的最小质因数一定是p,因为i的最小质因数一定大于等于p
然后我们就完美地实现了欧拉筛
代码走起:
void get_prime(int n){
memset(v,0,sizeof(v));
for (int i=2;i<=n;i++){
if (v[i]==0){
v[i]=i;
pr[++cnt]=i;
}
for (int j=1;j<=cnt;j++){
if (pr[j]>v[i]||pr[j]>n/i) break;
v[i*pr[j]]=pr[j];
}
}
}
目前为止,肯定还有很多人没有体会到这个算法的优秀之处,且听我一一道来:
首先它可以用O(log n)分解质因数,我们已经有了每个数的最小质因数,那我们岂不是可以对当前的v[n]的指数++,然后n/=v[n],直到n变成1了为止,有木有发现不用枚举了!!美滋滋
void fac(int n){
while(n>1){
r[v[n]]++;
n/=v[n];
}
}
如果你还没有被这个算法所折服,我再来告诉你,他可以O(n)筛法求出1~n的所有欧拉函数(什么?欧拉函数是什么?戳)
我们都知道欧拉函数的通向公式是:
φ
(
n
)
=
n
∗
(
1
−
1
p
1
)
∗
(
1
−
1
p
2
)
∗
(
1
−
1
p
3
)
∗
⋯
∗
(
1
−
1
p
s
)
\varphi(n)=n*(1-\frac{1}{p_1})*(1-\frac{1}{p_2})*(1-\frac{1}{p_3})* \cdots*(1-\frac{1}{p_s})
φ(n)=n∗(1−p11)∗(1−p21)∗(1−p31)∗⋯∗(1−ps1)
如果一个数n它只比m多一个质因数p,也就是
n
=
m
∗
p
n=m*p
n=m∗p,且m没有质因数p,那我们是不是就可以用这个式子算出
φ
(
n
)
\varphi(n)
φ(n):
φ
(
n
)
=
φ
(
m
)
∗
(
p
−
1
)
\varphi(n)=\varphi(m)*(p-1)
φ(n)=φ(m)∗(p−1)
如果不理解的话,简单推导一下:
如
果
φ
(
m
)
=
m
∗
(
1
−
1
p
1
)
∗
(
1
−
1
p
2
)
∗
(
1
−
1
p
3
)
∗
⋯
∗
(
1
−
1
p
s
)
如果\varphi(m)=m*(1-\frac{1}{p_1})*(1-\frac{1}{p_2})*(1-\frac{1}{p_3})* \cdots*(1-\frac{1}{p_s})
如果φ(m)=m∗(1−p11)∗(1−p21)∗(1−p31)∗⋯∗(1−ps1)
那
么
φ
(
n
)
=
m
∗
p
∗
(
1
−
1
p
1
)
∗
(
1
−
1
p
2
)
∗
(
1
−
1
p
3
)
∗
⋯
∗
(
1
−
1
p
s
)
∗
(
1
−
1
p
)
那么\varphi(n)=m*p*(1-\frac{1}{p_1})*(1-\frac{1}{p_2})*(1-\frac{1}{p_3})* \cdots*(1-\frac{1}{p_s})*(1-\frac{1}{p})
那么φ(n)=m∗p∗(1−p11)∗(1−p21)∗(1−p31)∗⋯∗(1−ps1)∗(1−p1)
上面下面比较一下,你就会发现下面就比上面多乘了一个
p
∗
(
1
−
1
p
)
p*(1-\frac{1}{p})
p∗(1−p1),就是
p
−
1
p-1
p−1
那么我们就可以用欧拉筛,在枚举小于v[i]的质数p,并更新 i ∗ p i*p i∗p时,有没有发现 i ∗ p i*p i∗p就是比i多了一个p,而且i没有p这个质因子(有可能有,因为当v[i]=p的时候我们也会去更新,等下在讲这个情况),于是乎,我们就可以用上面的方法更新了,也就是: φ ( i ∗ p ) = φ ( i ) ∗ ( p − 1 ) \varphi(i*p)=\varphi(i)*(p-1) φ(i∗p)=φ(i)∗(p−1)
BUT,v[i]=p的时候我们也会去更新 i ∗ p i*p i∗p,但是这个时候 i ∗ p i*p i∗p和i就都有质因子i了,这时候,看上面两个式子,求 φ ( i ∗ p ) \varphi(i*p) φ(i∗p)时后面就不用多乘一个 ( 1 − 1 p ) (1-\frac{1}{p}) (1−p1),但是前面要多乘一个p,在这种情况下,我们得到的是: φ ( i ∗ p ) = φ ( i ) ∗ p \varphi(i*p)=\varphi(i)*p φ(i∗p)=φ(i)∗p,判断一下即可
void get_phi(int n){
phi[1]=1;
memset(v,0,sizeof(v));
for (int i=2;i<=n;i++){
if (v[i]==0){
v[i]=i;
pr[++cnt]=i;
phi[i]=i-1;
}
for (int j=1;j<=cnt;j++){
if (pr[j]>v[i]||pr[j]>n/i) break;
v[i*pr[j]]=pr[j];
if (v[i]==pr[j]) phi[i*pr[j]]=phi[i]*pr[j];
else phi[i*pr[j]]=phi[i]*(pr[j]-1);
}
}
}
有没有瞬间觉得欧拉筛很nb!
——没有(逃
OK,完事
参考:《算法竞赛进阶指南》 李煜东 著

本文深入解析欧拉筛算法,一种高效的素数筛选及欧拉函数计算方法。相较于传统埃筛,欧拉筛实现O(n)复杂度,提供快速质因数分解与欧拉函数求解。通过维护最小质因数数组,实现合数仅被最小质因数筛一次,极大提升效率。


被折叠的 条评论
为什么被折叠?



