【总结】积性函数前缀和(杜教筛)

前言:

据CCH和LJH说,杜教筛似乎是一个非常套路的东西,几乎所有的杜教筛的题目推理方式都是一模一样的(但实测有些推理还是很恶心的)。所以复习杜教筛不需要太多时间,粗略看一遍,留下印象即可。


杜教筛其实是一种简化运算的推理方式,它的使用条件并不仅限于积性函数(?),只是积性函数可以将复杂度进一步优化。

例题:求欧拉函数前缀和

因为杜教筛是一种推理方式,所以直接给出例题反而容易上手。
欧拉函数的定义这里就不再赘述。
需要知道的是欧拉函数 (φ) ( φ ) 的一个特殊性质:

d|nφ(d)=n ∑ d | n φ ( d ) = n

形如 d|nf(d)=A ∑ d | n f ( d ) = A 这个性质是极为重要的,我们整个算法都是基于这个性质而来。

下面开始推导:

φ(n)=nd|n,d<nφ(d) φ ( n ) = n − ∑ d | n , d < n φ ( d )

那么
i=1inφ(i)=i=1in(id|i,d<iφ(d)) ∑ i = 1 i ≤ n φ ( i ) = ∑ i = 1 i ≤ n ( i − ∑ d | i , d < i φ ( d ) )

现在我们改变枚举变量:我们枚举 id i d 的值,即枚举i对d的倍数,因为i≠d,所以从2开始
i=1inφ(i)=n(n+1)2id=2idnd=1dnidφ(d) ∑ i = 1 i ≤ n φ ( i ) = n ∗ ( n + 1 ) 2 − ∑ i d = 2 i d ≤ n ∑ d = 1 d ≤ ⌊ n i d ⌋ φ ( d )

这时候就能够惊讶地发现:
如果设 Sum(n)=ini=1φ(i) S u m ( n ) = ∑ i = 1 i ≤ n φ ( i )
那么就可以写为:
Sum(n)=n(n+1)2id=2idnSum(nid) S u m ( n ) = n ∗ ( n + 1 ) 2 − ∑ i d = 2 i d ≤ n S u m ( ⌊ n i d ⌋ )

我们成功地将问题化简了!为了方便起见,我们将这个 id i d 写为 i i
Sum(n)=n(n+1)2i=2inSum(ni) S u m ( n ) = n ∗ ( n + 1 ) 2 − ∑ i = 2 i ≤ n S u m ( ⌊ n 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
#include
#include
#include
#include
#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];
vectorlong 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;iif(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));
}

莫比乌斯函数求前缀和

我们类比欧拉函数前缀和的取值方式,其实莫比乌斯函数也有一个类似的性质:

d|nμ(d)=[n=1] ∑ d | n μ ( d ) = [ n = 1 ]

那么,同理:
μ(n)=[n=1]d|n,d<nμ(d) μ ( n ) = [ n = 1 ] − ∑ d | n , d < n μ ( d )

一样的套路:
i=1inμ(i)=n1i=1ind|i,d<iμ(d) ∑ i = 1 i ≤ n μ ( i ) = n − 1 − ∑ i = 1 i ≤ n ∑ d | i , d < i μ ( d )

仍然是一样的套路:
i=1inμ(i)=n1id=2idnd=1dnidμ(d) ∑ i = 1 i ≤ n μ ( i ) = n − 1 − ∑ i d = 2 i d ≤ n ∑ d = 1 d ≤ ⌊ n i d ⌋ μ ( d )

然后也是一样的套路:
Sum(n)=ini=1μ(i) S u m ( n ) = ∑ i = 1 i ≤ n μ ( i )
仍然将 id i d 写作 i i
于是就可以得到最终的表达式:
Sum(n)=n1i=2inSum(ni) S u m ( n ) = n − 1 − ∑ i = 2 i ≤ n S u m ( ⌊ n i ⌋ )

最后的最后,也是将前 n23 n 2 3 线性筛求出,复杂度也为 O(n23) O ( n 2 3 )

套路基本就是这些,有些恶心题会在此基础上套一些变换,使得杜教筛看起来不那么板。那些就基本上只能看数(ren)学(pin)能力和经验了。

模板题

#include
#include
#include
#include
#include
#include
#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 >ans[MAXP+10];
long long get_num(long long x){
    found=1;
    long long px=x%MAXP;
    for(int i=0;i
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));
}

你可能感兴趣的:(数学,总结)