zoukankan      html  css  js  c++  java
  • IT发烧友,一个真正的技术交流群

    杜教筛

    (似乎有很多人在催我的杜教筛呢......)


    前言

    • 话说,我是不是在自己的莫比乌斯反演中挖了许多杜教筛的坑啊......
    • 本文完整的总结介绍杜教筛,也算是将莫比乌斯反演中的坑全部填满吧!
    • 真诚地希望来阅读这篇学习笔记的每一个人,仔仔细细的看完每一段。
    • 我相信,只要认真的看完整篇文章并跟着一起思考的读者,一定能够有所收获!
    • 如果您之前不会杜教筛,那么我希望这篇文章能够作为您学习杜教筛路上的有力援助,帮助您真正的了解与掌握杜教筛!
    • 如果您之前早已熟知杜教筛或只有些模糊的印象,相信您一定也能有所收获!
    • (PS:本文较长,请耐心阅读 ovo)

    在OI中的意义

    • 其实,对于一般的数论题,线性筛已经非常的优秀了。
    • 但是就是有那些(duliu)出题人,硬是要把数据出到(1e10)之类的,就看你会不会杜教筛,min_25筛,洲阁筛等各种神奇的筛法。(PS:后面这两个筛法我是真的不会了QAQ)
    • 要是不会,那就要少十分左右!
    • 所以,专门用杜教筛来推式子的题目很少,一般都是用杜教筛优化线性筛,弄到最后的那些分。
    • 不过,杜教筛的思想对于推式子是很有帮助的。(它那种递归求解的形式,以及复杂度(O(n^frac{2}{3}))
    • 因此,学会杜教筛也是一件挺好的事情!

    前置技能

    • 杜教筛的前置技能挺多的......

    各种函数

    概念

    • 首先,我们需要知道有一个东西叫做数论函数
    • 数论函数有很多种,但是我们身为Oier,并不需要知道它的具体的定义,具体的分类。
    • 我们只需要知道,我们在OI中的数论中所用到的各种函数(mu,varphi)等都是数论函数。(后面会将常见的都列举出来,当然不只这两种)。
    • 当你了解数论函数后,你就需要知道有一类函数叫做积性函数
    • 还是同样的话语,我们平常所惯用的数论函数都是积性函数!
    • 不过,对于积性函数的定义还是有必要了解一下。(毕竟有些函数看上去不常见,其实可能就是积性函数!)
    • 积性函数定义:如果已知一个函数为数论函数,且(f(1)=1),并且满足以下条件,若对于任意的两个互质的正整数(p,q)都满足(f(pcdot q)=f(p)cdot f(q)),那么则称这个函数为积性函数
    • 特殊的,如果当对于任意的正整数(p,q)(即不一定互质),也满足以上这个式子,则称这个函数为完全积性函数
    • 而我们的杜教筛,则是用来筛积性函数前缀和的神奇筛法!!!
    • 说了这么多概念性的东西,不如来点实质性的!

    常见积性函数

    1. (mu(n))——莫比乌斯函数。关于这个函数,我在莫比乌斯反演中说的挺清楚的了(233),(PS:不过我将会在下文中,从另一种角度介绍它的性质。也算是把坑给填完吧)
    2. (varphi(n))——欧拉函数。表示不大于(n)且与(n)互质的正整数个数,十分常见的数论函数。用数学式子表示即:(varphi(n)=sum_{i=1}^{n}[(n,i)=1]) (PS:((n,i))表示(gcd(n,i)))
    3. (d(n))——约数个数。表示(n)的约数的个数。用式子表示为:(d(n)=sum_{d|n}1),也可以写作:(d(n)=sum_{d=1}^{n}[d|n]) (其实没什么太大区别啦!)
    4. (sigma(n))——约数和函数。 即(n)的各个约数之和。表示为:(sigma(n)=sum_{d|n}d=sum_{d=1}^{n}[d|n]cdot d)

    (PS:接下来列举的是完全积性函数)
    (PS:代表字母可能会与他人的略有不同,似乎在数学中没有统一的字母)

    1. (epsilon(n))——元函数。似乎也有人把它叫作(e(n))?其实无所谓啦~~我们只需要知道(epsilon(n)=[n=1])。(看到这个是不是有种莫名的熟悉感呢?到了下文中,就会发现这种熟悉感是从哪来的啦!)
    2. (I(n))——恒等函数。所谓恒等就是这个函数的值恒为(1)
    3. (id(n))——单位函数。(id(n)=n)

    (当第一次看到这些完全积性函数的时候,是不是有人感觉这些完全积性函数毫无用处,都是一些简单的式子,只不过用符号表示了呢?在下一个前置技能——狄利克雷卷积中,你应该就会改变自己(naive)的想法啦~)

    狄利克雷卷积 ((*))

    基本知识

    • 听名字,似乎是一个很高深的东西。
    • 其实,若是不理睬这个名字,只是把它当作一个新定义的符号,你应该就会发现,狄利克雷卷积也不是那样的难理解。
    • 定义:两个数论函数(f)(g)的卷积为((f*g)(n)=sum_{d|n}f(d) cdot g(frac{n}{d}))。前面的括号代表将(f)(g),后面的括号代表范围。(PS:后面的括号一般可以省略不写,默认为(n))
    • 很显然,狄利克雷卷积满足以下运算规律:
    1. 交换律((f*g=g*f));
    2. 结合律(((f*g)*h=f*(g*h)));
    3. 分配律(((f+g)*h=f*h+g*h));
    • 在记忆方面,可以类比为乘法的运算法则,其实上面这几条运算规律是可以证明的!
    • 举个例子,交换律。我们看狄利克雷卷积的式子,实质上就是(n)的每一个约数带入(f)中的值,乘上与之对应的约数在(g)中的值。
    • 显然,当交换(f)(g)时,仅仅时枚举约数的顺序发生了改变,而每一个约数对答案的贡献是不会有改变的。因此存在交换律!
    • 在大致了解了狄利克雷卷积的运算法则后,我们就需要提到上面所说的积性函数啦!
    • 首先,元函数 (epsilon)。所谓元函数,指的就是在狄利克雷卷积中充当单位元的作用,单位元即满足:(f*epsilon=f)。不要小看这个元函数,当元函数配合上结合律时就可以用来证明一些结论啦~
    • 除了元函数之外,我们最为常见的则是(mu,varphi)之类的的函数,因此我们需要十分熟练它们与一些常见的完全积性函数的卷积,以及性质。

    (PS:特别要记住一点:积性函数有一个特别重要的性质,那就是(积性函数(*)积性函数)仍然为积性函数!!!这个性质可以用来判断能否被杜教筛!)

    莫比乌斯函数(mu)

    • (mu)。在莫比乌斯反演中,我们曾了解过一个与(mu)有关的性质:(sum_{d|n}mu(d)=[n=1])
    • 我们将这个性质表示成狄利克雷卷积的形式即:(mu*I=epsilon)。这在狄利克雷卷积中是一个很常用的恒等式。当然,有了它,我们也能够证明出莫比乌斯反演啦!
    • 开始填坑,证明莫比乌斯反演:
      已知:

    [F(n)=sum_{d|n}f(d) ]

    用狄利克雷卷积的形式表示这个式子即:(F=f*I)
    利用狄利克雷卷积将(F)卷上(mu),得到:

    [F*mu=f*I*mu ]

    由于狄利克雷卷积具有结合律与交换律,因此原式可化为:

    [ o f*(I*mu)=f*epsilon=f ]

    即:(f=F*mu)。代入后即可证明莫比乌斯反演:(f(n)=sum_{d|n}mu(d)cdot F(frac{n}{d}))
    同理,自然也可以得到莫比乌斯反演的另一种形式:(f(n)=sum_{n|d}mu(frac{d}{n})cdot F(d))
    (总算填完一个大坑......)

    欧拉函数 (varphi)

    • (varphi)。欧拉函数有一个很著名的性质:(sum_{d|n}varphi(d)=n)
    • 与以上方法类似,我们将它表示成狄利克雷卷积的形式:(varphi*I=id)
    • 这时候,看到这个式子我们会有一个大胆的想法,既然在这个欧拉函数与莫比乌斯函数的式子中都有(I),那么我们不如将这个式子的两边同时卷上一个(mu)
    • 于是,我就可以开始填第二个坑了——欧拉函数与莫比乌斯函数的关系。

    [varphi*I=id \ o varphi*I*mu=id*mu \ o varphi*epsilon=id*mu ]

    即:(varphi=id*mu o varphi(n)=sum_{d|n}mu(d)cdot frac{n}{d})

    • 我们把这个式子的两边同时除以(n),则可以推出这个巧妙的式子:

    [frac{varphi(n)}{n}=sum_{d|n}frac{mu(d)}{d} ]

    (至此,我终于把莫比乌斯反演中的坑填完啦~~23333)
    (有关杜教筛的前置技能也说的差不多啦,终于可以步入正题啦!)


    步入正题——杜教筛

    • 说了这么久,终于可以开始讲杜教筛啦!(是不是有一种莫名的兴奋呢?)
    • 首先,我们应该弄清楚一个问题:杜教筛到底是用来干什么的?
    • 杜教筛是以低于线性的时间复杂度来计算积性函数的前缀和的神奇筛法!
    • 即我们需要计算的式子为:(sum_{i=1}^{n}f(i)) ((f(i))为积性函数)
    • PS:接下来要讲解的是杜教筛的套路式,如果不懂为什么要这样做,也没有关系。只需要明白它是怎么推过来的就行了。实在看不懂就记个结论吧......
    • 推式子时间到!
    • 为了解决这个问题,我们构造两个积性函数(h)(g)。使得(h=f*g)
    • 现在我们开始求(sum_{i=1}^{n}h(i))
    • (S(n)=sum_{i=1}^{n}f(i))

    [sum_{i=1}^{n}h(i)=sum_{i=1}^{n}sum_{d|i}g(d)cdot f(frac{i}{d})\ o =sum_{d=1}^{n}g(d)cdotsum_{i=1}^{lfloorfrac{n}{d} floor}f({i}) ]

    [ o sum_{i=1}^{n}h(i)=sum_{d=1}^{n}g(d)cdot S(lfloorfrac{n}{d} floor) ]

    接着,我们将右边式子的第一项给提出来,可以得到:

    [sum_{i=1}^{n}h(i)=g(1)cdot S(n)+sum_{d=2}^{n}g(d)cdot S(lfloorfrac{n}{d} floor) ]

    [ o g(1)S(n)=sum_{i=1}^{n}h(i)-sum_{d=2}^{n}g(d)cdot S(lfloorfrac{n}{d} floor) ]

    其中的(h(i)=(f*g)(i));

    • 这就是杜教筛的惯用套路式。经各种分析,只要当你的(h(i))的前缀和很好求,能在较短的时间内求出,那么当我们对后面的式子进行整除分块时,求(S(n))的复杂度为(O(n^{frac{2}{3}}))
    • 当我们知道了这个套路式后,可能会思考,我们应该如何选择这个(g)(h)呢?
    • 对于这个疑问,我没有太好的回答。只能说,依靠平时对于狄利克雷卷积中的各种式子的熟悉,以及仔细观察式子的能力啦!(当然我下面也会介绍一种小方法啦~~OVO)
    • 知道了这个套路式总要练练手吧!

    应用

    (PS:以下例子中,假设线性筛均跑不过)

    一:求(S(n)=sum_{i=1}^{n}mu(i))

    • 根据那个套路式:(g(1)S(n)=sum_{i=1}^{n}(f*g)(i)-sum_{d=2}^{n}g(d)cdot S(lfloorfrac{n}{d} floor)),我们只需要找一个积性函数(g)使得这个函数与(mu)的卷积的前缀和容易求。如果你认真的看了上文,应该就可以很轻松的想到一个积性函数(I)
    • 我们知道(mu*I=epsilon),很显然,单位元的前缀和非常好求,就是(1),并且(I)十分方便整除分块。所以我们把这个积性函数带入上述式子中可以得到:

    [S(n)=1-sum_{d=2}^{n}S(lfloorfrac{n}{d} floor) ]

    因此,我们就学会了杜教筛莫比乌斯函数的前缀和啦!

    二:求(S(n)=sum_{i=1}^{n}varphi(i))

    • 与求莫比乌斯函数的思路类似。
    • 我们在脑海中找到一个与欧拉函数有关的卷积式子:(varphi*I=id)
    • 我们可以发现,在筛欧拉函数前缀和所选择的积性函数(g)同样也是(I)哟!代入得:

    [S(n)=sum_{i=1}^{n}i-sum_{d=2}^{n}S(lfloorfrac{n}{d} floor) ]

    前面那个式子可以利用等差数列求和公式(O(1))的计算出结果,后面同样利用整除分块。
    所以,我们又学会了如何筛欧拉函数的前缀和啦!

    三:求(S(n)=sum_{i=1}^{n}icdot varphi(i))

    • 这个式子是不是无法一眼看出需要配什么积性函数了呢?
    • 我们考虑狄利克雷卷积的形式:(sum_{d|n}(dcdotvarphi(d))cdot g(frac{n}{d}))
    • 我们看前面这个(d)不太爽,考虑后面配出一个积性函数使得这个(d)能够被约掉。因此,我们尝试将(g)配成(id)。这样就可以把(d)给弄没!代入得:

    [sum_{d|n}(dcdotvarphi(d))cdot frac{n}{d}=sum_{d|n}ncdotvarphi(d)\ o=nsum_{d|n}varphi(d)=n^2 ]

    我们惊喜的发现,似乎配对了!!!
    得:

    [S(n)=sum_{i=1}^{n}i^2-sum_{d=2}^{n}dcdot S(lfloorfrac{n}{d} floor) ]

    对于这个式子,我们前面可以利用平方和的公式(O(1))算出结果,后面的式子利用等差数列求和公式进行整除分块。
    因此,我们可以通过以上的思路求得这个看似无法筛的积性函数的前缀和!

    代码实现

    • 至于在信息学中的代码实现,我给出一个大概的思路:我们首先先线筛出数据范围根号左右的积性函数的前缀和。再递归的实现杜教筛。
    • 特别要注意的是,杜教筛筛出的前缀和一定要存下来!!!
    • 如果你比较的勤劳,那就去手写hash,如果你想偷懒,那就最好用stl中的unordered_map,最好不要用map,平白无故多个log的复杂度,何必呢......
    • 还有一点,一定要记得取模!!!以及,判断要不要开long long,搞不好你TLE就是因为取模去多了,或者long long开多啦!
    • 有评论区的大佬提醒我,说这份代码被卡了,我调了一下前面线筛的范围,有一定的加速,最后发现,果然是开long long的锅,现在已经将代码改正,是没有问题的啦!
    • 在这里我就粘一下自己杜教筛(mu)(varphi)的板子吧。这种东西最好自己手打一遍,不然你一没注意,常数一大,就很麻烦啦!(反正我写这个东西,常数巨大)
    • 因此,代码仅供参考!luoguP4213杜教筛模板
    #include<bits/stdc++.h>
    #include<tr1/unordered_map>
    #define N 6000010
    using namespace std;
    template<typename T>inline void read(T &x)
    {
        x=0;
        static int p;p=1;
        static char c;c=getchar();
        while(!isdigit(c)){if(c=='-')p=-1;c=getchar();}
        while(isdigit(c)) {x=(x<<1)+(x<<3)+(c-48);c=getchar();}
        x*=p;
    }
    bool vis[N];
    int mu[N],sum1[N],phi[N];
    long long sum2[N];
    int cnt,prim[N];
    tr1::unordered_map<long long,long long>w1;
    tr1::unordered_map<int,int>w;
    void get(int maxn)
    {
        phi[1]=mu[1]=1;
        for(int i=2;i<=maxn;i++)
        {
            if(!vis[i])
            {
                prim[++cnt]=i;
                mu[i]=-1;phi[i]=i-1;
            }
            for(int j=1;j<=cnt&&prim[j]*i<=maxn;j++)
            {
                vis[i*prim[j]]=1;
                if(i%prim[j]==0)
                {
                    phi[i*prim[j]]=phi[i]*prim[j];
                    break;
                }
                else mu[i*prim[j]]=-mu[i],phi[i*prim[j]]=phi[i]*(prim[j]-1);
            }
        }
        for(int i=1;i<=maxn;i++)sum1[i]=sum1[i-1]+mu[i],sum2[i]=sum2[i-1]+phi[i];
    }
    int djsmu(int x)
    {
        if(x<=6000000)return sum1[x];
        if(w[x])return w[x];
        int ans=1;
        for(int l=2,r;l>=0&&l<=x;l=r+1)
        {
            r=x/(x/l);
            ans-=(r-l+1)*djsmu(x/l);
        }
        return w[x]=ans;
    }
    long long djsphi(long long x)
    {
        if(x<=6000000)return sum2[x];
        if(w1[x])return w1[x];
        long long ans=x*(x+1)/2;
        for(long long l=2,r;l<=x;l=r+1)
        {
            r=x/(x/l);
            ans-=(r-l+1)*djsphi(x/l);
        }
        return w1[x]=ans;
    }
    int main()
    {
        int t,n;
        read(t);
        get(6000000);
        while(t--)
        {
            read(n);
            printf("%lld %d
    ",djsphi(n),djsmu(n));
        }
        return 0;
    }
    

    总结

    • 当然,杜教筛还能够筛许多东西,如:(sum_{i=1}^{n}i^2cdotmu(i))之类的一系列积性函数,在这里就不一一列举啦。
    • 其实,一般来说筛的就是那些常用的积性函数。
    • 如果实在碰到类似与上面那个无法一眼看出结果的式子,我们就可以采用刚刚例三的思路.
    • 先考虑将那些特殊性质不明显的数弄掉,再尝试猜积性函数。当然不一定一试就中,但是只要我们有足够的耐心与信念,相信这个题目所给的一定能筛,就一定能试出来233.
    • 当然还有一种方法,从常见的完全积性函数开始试,如果都不行,在尝试一下高次的完全积性函数,之后尝试非完全积性函数(虽说一般都不是这个。。。),如果还是不行,那就算了吧,(反正就那一点分么。。。),试不出,技不如人,甘拜下风233.

    题目

    • 题目可以去51nod上找,那上面杜教筛的题目挺多的,我就不粘地址啦!
    • 洛谷上也有模板题!
    • 当然,洛谷上也有需要推式子的题目,我以后有时间再加吧!
  • 相关阅读:
    归并排序
    快速排序
    冒泡排序
    排序算法复杂度
    [LeetCode] 20. Valid Parentheses ☆(括号匹配问题)
    makefile编写helloworld
    shell的通俗理解
    PID三种参数的理解
    PID的原理
    PID控制温度
  • 原文地址:https://www.cnblogs.com/peng-ym/p/9446555.html
Copyright © 2011-2022 走看看