前言:
据CCH和LJH说,杜教筛似乎是一个非常套路的东西,几乎所有的杜教筛的题目推理方式都是一模一样的(但实测有些推理还是很恶心的)。所以复习杜教筛不需要太多时间,粗略看一遍,留下印象即可。
杜教筛其实是一种简化运算的推理方式,它的使用条件并不仅限于积性函数(?),只是积性函数可以将复杂度进一步优化。
例题:求欧拉函数前缀和
因为杜教筛是一种推理方式,所以直接给出例题反而容易上手。
欧拉函数的定义这里就不再赘述。
需要知道的是欧拉函数
(φ)
(
φ
)
的一个特殊性质:
形如 ∑d|nf(d)=A ∑ d | n f ( d ) = A 这个性质是极为重要的,我们整个算法都是基于这个性质而来。
下面开始推导:
那么
现在我们改变枚举变量:我们枚举 id i d 的值,即枚举i对d的倍数,因为i≠d,所以从2开始
这时候就能够惊讶地发现:
如果设 Sum(n)=∑i≤ni=1φ(i) S u m ( n ) = ∑ i = 1 i ≤ n φ ( i )
那么就可以写为:
我们成功地将问题化简了!为了方便起见,我们将这个 id i d 写为 i i
接下来,只需要按照根据 ⌊ni⌋ ⌊ n i ⌋ 的不同取值来分块,就可以达到 O(n34) O ( n 3 4 ) 的复杂度。
显然这还不够优秀,那么针对与积性函数,我们还有一个小优化:
将 Sum(1),Sum(2),Sum(3)...Sum(n23) S u m ( 1 ) , S u m ( 2 ) , S u m ( 3 ) . . . S u m ( n 2 3 ) 都用线性筛预先求出来,这样一来,时间复杂度可以优化到 O(n23) O ( n 2 3 ) 这样的复杂度就很不错了
这就是一个很经典的杜教筛的题目。
模板题
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<vector>
#define MAXN 500010
#define MAXP 500010
#define MOD 1000000007
#define SF scanf
#define PF printf
using namespace std;
bool isprime[MAXN];
int primes[MAXN],cnt;
long long fi[MAXN];
vector<pair<long long,long long> > ans[MAXP+10];
void prepare(){
fi[1]=1;
for(int i=2;i<=MAXN-10;i++){
fi[i]%=MOD;
if(isprime[i]==0){
primes[++cnt]=i;
fi[i]=i-1;
}
for(int j=1;j<=cnt&&i*primes[j]<=MAXN-10;j++){
isprime[i*primes[j]]=1;
if(i%primes[j]==0){
fi[i*primes[j]]=fi[i]*primes[j];
break;
}
fi[i*primes[j]]=fi[i]*fi[primes[j]];
}
}
for(int i=2;i<=MAXN-10;i++)
fi[i]=(fi[i-1]+fi[i])%MOD;
}
long long get_num(long long n){
long long px=n%MAXP;
for(int i=0;i<ans[px].size();i++)
if(ans[px][i].first==n)
return ans[px][i].second;
return -1;
}
void push_num(long long n,long long res){
long long px=n%MAXP;
ans[px].push_back(make_pair(n,res));
}
long long sum(long long n){
if(n<=MAXN-10)
return fi[n];
long long res=get_num(n);
if(res>=0)
return res;
if(n%2==0)
res=(((n/2)%MOD)*((n+1ll)%MOD))%MOD;
else
res=((n%MOD)*(((n+1ll)/2)%MOD))%MOD;
for(long long p=2;p<=n;){
long long len=n/(n/p)+1ll-p;
res=(res-len*sum(n/p))%MOD;
if(res<0)
res+=MOD;
p+=len;
}
push_num(n,res);
return res;
}
int main(){
prepare();
long long n;
/*for(int i=1;i<=10000;i++)
if(fi[i]!=sum(i)){
PF("Error!%d: %d %d\n",i,fi[i],sum(i));
break;
}*/
SF("%lld",&n);
PF("%lld",sum(n));
}
莫比乌斯函数求前缀和
我们类比欧拉函数前缀和的取值方式,其实莫比乌斯函数也有一个类似的性质:
那么,同理:
一样的套路:
仍然是一样的套路:
然后也是一样的套路:
令 Sum(n)=∑i≤ni=1μ(i) S u m ( n ) = ∑ i = 1 i ≤ n μ ( i )
仍然将 id i d 写作 i i
于是就可以得到最终的表达式:
最后的最后,也是将前 n23 n 2 3 线性筛求出,复杂度也为 O(n23) O ( n 2 3 )
套路基本就是这些,有些恶心题会在此基础上套一些变换,使得杜教筛看起来不那么板。那些就基本上只能看数(ren)学(pin)能力和经验了。
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<vector>
#include<map>
#define SF scanf
#define PF printf
#define MAXN 5000010
#define MAXP 2181271
using namespace std;
bool isprime[MAXN],found;
int primes[MAXN],cnt,mu[MAXN];
map<long long,int> ans;
/*vector<pair<long long,long long> >ans[MAXP+10];
long long get_num(long long x){
found=1;
long long px=x%MAXP;
for(int i=0;i<ans[px].size();i++)
if(ans[px][i].first==x)
return ans[px][i].second;
found=0;
return 0;
}
void push_num(long long x,long long val){
long long px=x%MAXP;
ans[px].push_back(make_pair(x,val));
}*/
void prepare(){
mu[1]=1;
for(int i=2;i<=MAXN-10;i++){
if(isprime[i]==0){
mu[i]=-1;
primes[++cnt]=i;
}
for(int j=1;j<=cnt&&primes[j]*i<=MAXN-10;j++){
isprime[i*primes[j]]=1;
if(i%primes[j]==0){
mu[i*primes[j]]=0;
break;
}
mu[i*primes[j]]=-mu[i];
}
}
for(int i=1;i<=MAXN-10;i++)
mu[i]=mu[i-1]+mu[i];
}
long long l,r;
long long sum(long long n){
if(n<=MAXN-10)
return mu[n];
if(ans.count(n)!=0)
return ans[n];
/*long long res=get_num(n);
if(found==1)
return res;*/
long long res=1ll;
for(long long q=2;q<=n;){
long long len=n/(n/q)+1ll-q;
res-=sum(n/q)*len;
q+=len;
}
ans[n]=res;
//push_num(n,res);
return res;
}
int main(){
prepare();
SF("%lld%lld",&l,&r);
PF("%lld",sum(r)-sum(l-1));
}