前置知识
积性函数
顾名思义,积性函数是一类满足
f(ab)=f(a)×f(b) 的函数,当然
f(ab)=f(a)×f(b) 是有成立条件的,它的成立条件是
gcd(a,b)=1。
线性筛
可以用
O(n) 的时间复杂度筛出积性函数
f(x),x∈[1,n] 的值。
既然都来学习杜教筛了,一定会线性筛了吧。
代码如下:
CPPconst int N=1e7;
bool pri[N];
int prime[N],mul[N],phi[N],cnt[N],idx;
inline void sieve(int x){
mul[1]=1;
phi[1]=1;
memset(pri,true,sizeof pri);
for(int i=2;i<=x;i++){
if(pri[i]) prime[++idx]=i,mul[i]=-1,phi[i]=i-1,cnt[i]=1;
for(int j=1;j<=idx&&prime[j]*i<=x;j++){
pri[i*prime[j]]=false;
if(i%prime[j]) mul[i*prime[j]]=-mul[i],phi[i*prime[j]]=phi[i]*(prime[j]-1),cnt[i*prime[j]]=cnt[i]+1;
else{mul[i*prime[j]]=0,phi[i*prime[j]]=phi[i]*prime[j],cnt[i*prime[j]]=cnt[i];break;}
}
}
}
狄利克雷卷积
存在算术函数
f 和
g,其狄利克雷卷积记为
f∗g,满足:
(f∗g)(n)=∑d∣nf(d)×g(dn)
或
(f∗g)(n)=∑ab=nf(a)×g(b)
两种情况本质上是一致的,只是两种不同的写法。
- μ∗I=ϵ 源于 d∣n∑μ(d)=[n=1]
- φ∗I=id 源于 d∣n∑φ(d)=n
- μ∗id=φ 源于 φ(n)=i=1∑n[gcd(i,n)=1]=d∣n∑dnμ(d)
- I∗id=σ
其中
I(x)=1,ϵ(x)=[x=1],id(x)=x。
整除分块
对于
⌊ln⌋ 明显存在一个区间
[l,r] 使得
∀i∈[l,r],⌊in⌋=⌊ln⌋,若给定
l,
r 的最大值是
⌊⌊ln⌋n⌋,如果将
⌊in⌋ 值相同的
i 分为一个块,块的数量小于
2n,可以大大优化形似
i=1∑nf(i)×g(⌊in⌋) 求和公式计算的时间复杂度,详见
例题。
杜教筛
杜教筛是一种用于求数论函数前缀和的非线性算法,可以在
O(n32) 的时间复杂度内求出
S(n)=i=1∑nf(i) 的值。
为了快速求
f(x) 的前缀和,我们可以为它找到一个函数
g(x) 使得
h(x)=(f∗g)(x) 与
g(x) 的前缀和是好求的,这时就可以用狄利克雷卷积对式子进行一些变换了:
i=1∑nh(i)=i=1∑nd∣i∑g(d)f(dn)=d=1∑ng(d)d∣i∑nf(di)=d=1∑ng(d)i=1∑⌊dn⌋f(i)=d=1∑ng(d)S(⌊dn⌋)
其中记
S(x)=i=1∑xf(i)
于是我们可以得到:
i=1∑nh(i)=d=1∑ng(d)S(⌊dn⌋)
i=1∑nh(i)=g(1)S(n)+d=2∑ng(d)S(⌊dn⌋)
g(1)S(n)=i=1∑nh(i)−d=2∑ng(d)S(⌊dn⌋)
根据这个式子我们可以写出如下伪代码:
CPPlong long get_fsum(int x){
long long res=sumh(x);
for(int i=2;i<=x;i=r+1){
int r=x/(x/i);
res-=(sumg(r)-sumg(i-1))*get_fsum(x/i);
}
return res;
}
我们假设
g,h 求前缀和的时间复杂度是
O(1) 的,那么这样的杜教筛时间复杂度是
O(n43) 的,证明如下:
设在
x 时我们计算
S(x) 附加计算的
S(y),y=⌊ix⌋ i=2,3,...,n 其中
y 构成的集合为
T(x)={y∣y=⌊ix⌋,i=2,3,...,x},根据整除分块的结论,我们可以得到
∀z∈T(x) 必然有
T(z)⊆T(x),我们可以记忆化记录
∀z∈T(n) 的
S(z) 即可,而这样的
z 个数仅有
n 个,计算
S(z) 的时间复杂度是基于整除分块的
O(z) 的,记
T′(n)=T(n)∪{n},时间复杂度即为:
O(∑z∈T′(n)z)
我们可以将
T′(n) 拆为
A={x∣x∈T′(n),x>n} 与
B={x∣x∈T′(n),x≤n}
对于
A 部分:
∑z∈Az≈i=1∑nin=ni=1∑ni1
已知
i=1∑ki1∼2k,所以:
∑z∈Az≈n⋅2n=2n43
对于
B 部分
∑z∈Bz=∫1nz dz=32z231n
分别带入上下界得:
∑z∈Bz=32z231n=32(z43−1)
于是总时间复杂度为:
O(∑z∈T′(n)z)=O(2n43+32(z43−1))=O(n43)
我们还可以继续优化杜教筛,我们先用线性筛筛出
i≤n32 的
S(i),此时线性筛与杜教筛的时间复杂度均为
O(n32),达到最优,证明过程与上面类似。
实际应用
接下来我们回归题目,对于求
μ 的前缀和就可以用前置知识中给出的
μ∗I=ϵ,这非常容易实现。同理求
φ 的前缀和就可以用
φ∗I=id,依旧容易实现,接下来给出代码。
CODE
CPP#include<bits/stdc++.h>
using namespace std;
#define int long long
int t,pri[1000005],prime[1000005],idx,mul[1000005],phi[1000005];
unordered_map<int,int>mull,phii,book_mul,book_phi;
const int len=1000000;
void init(){
mul[1]=1;
phi[1]=1;
memset(pri,true,sizeof pri);
for(int i=2;i<=len;i++){
if(pri[i]) {prime[++idx]=i;mul[i]=-1;phi[i]=i-1;}
for(int j=1;j<=idx&&i*prime[j]<=len;j++){
pri[i*prime[j]]=false;
if(i%prime[j]){mul[i*prime[j]]=-mul[i];phi[i*prime[j]]=phi[i]*(prime[j]-1);}
else{mul[i*prime[j]]=0;phi[i*prime[j]]=phi[i]*prime[j];break;}
}
}
for(int i=1;i<=len;i++)mul[i]+=mul[i-1],phi[i]+=phi[i-1];
}
int get_mul(int x){
if(x<=len)return mul[x];
else if(book_mul[x]) return mull[x];
int r,res=1;
for(int i=2;i<=x;i=r+1){
r=x/(x/i);
res-=(r-i+1)*get_mul(x/i);
}
book_mul[x]=1,mull[x]=res;
return res;
}
int get_phi(int x){
if(x<=len)return phi[x];
else if(book_phi[x]) return phii[x];
int r,res=(x+1)*x/2;
for(int i=2;i<=x;i=r+1){
r=x/(x/i);
res-=(r-i+1)*get_phi(x/i);
}
book_phi[x]=1,phii[x]=res;
return res;
}
signed main(){
init();
cin>>t;
while(t--){
int n;
cin>>n;
cout<<get_phi(n)<<" "<<get_mul(n)<<endl;
}
return 0;
}