zoukankan      html  css  js  c++  java
  • 杜教筛使用心得

    现在杜教筛好像是个人都会了 orz。

    所以我写篇博客复习一下,这里不讲原理只讲优化技巧的和如何使用

    先贴两个学习原理的博客:

    http://www.cnblogs.com/abclzr/p/6242020.html
    http://www.cnblogs.com/candy99/p/dj_s.html

    然后我们来讲一下,怎么使用,下面均以要 求和的n为109 为讨论前提

    常用公式

    一般取用来卷积的辅助函数g为恒等函数:g(n)=1 ,n=1,2,3,…………(有时取g(n)=mu(n) 啥的也有奇效,反正是玄学)

    这时杜教筛的公式就能化简为:

     我们一般只用红框里的两个公式。

    杜教筛不要求函数一定要有积性,而是要求两点

    1. f(i)在106以内的所有前缀和能在接近O(n)复杂度内全部处理出来。
    2. 狄利克雷卷积的前缀和——h(n) ,在1e9 内的任意一项h(m),能在O(sqrt(m)) 以内的复杂度内求出来。

    使用步骤一:预处理106 以内的s[i]

    这一步如果f(i)是积性函数,可以直接用线性筛法直接搞定,如果不是的话,就要可能要注意分析性质瞎搞了(比如搞一些预处理,使得f(i)可以O(1)求之类的骚操作)。

    注意这里只是以106为例,因为后面的计算过程常数比较大,追求速度的话,最好要能处理到2*106

    使用步骤二:写快速计算h函数第k项的函数 h(int k)

    这里求h,有以下几种方法

    1. 用公式推出的通项看是否有优良的性质。可以快速搞出前n项和

    2. 打表找规律
    3. 打表找规律

     使用步骤三:写个记忆化搜索计算s(n)

     设我们预处理出s[i]的范围是i∈[1,UB】。

    通用的写法

    若之前算过n 我们则返回我们记下的值,因为n比较大不能用数组直接存,最好用哈希表。

    若n<=UB,我们返回预处理好的s[i]

    否则使用公式递归计算

    要注意到(n/i) 只有2*sqrt(n) 不同的取值,直接用一下数论分块就行了(如果不知道啥是数论分块的话,你百度一下就行 了)

    然后注意把值存到哈希表内。

    代码如下

     1 long long S(long long n)
     2 {
     3     if(n<=UB)
     4     {
     5         return s[n];
     6     }
     7     long long ans=0;
     8     ans=table.find(n);///查询哈希表中是否有n
     9     if(ans==-1)
    10     {
    11         long long i,j;
    12         ans=0;
    13         for(i=2; i<=n; i++)
    14         {
    15             j=i;
    16             i=n/(n/i);
    17             ans+=(i-j+1)*S(n/j);
    18         }
    19         ans=(h(n)-ans);
    20         table.insert(n,ans);/// 将n对应的值存入哈希表
    21     }
    22     return ans;
    23 }
    适合多组查询的哈希表写法

    更方便的写法

      但是如果只有单组,或者你不最求速度,这里有通过构造一个完美哈希把哈希表扔掉,且非递归的写法。

    首先我发现递归的时候是从大递归到小的,跟dp原理递推的方法一样,我可以反过来从小算到大,这样就能保证每次转移时子状态已经算好了。

    然后问题的关键在于有多少种不同状态,可以发现递归时,n是不断被除的,当前状态即x=n /a1/a2/a3/……/ak       这到底会有多少种不同的取值呢? 答案是2*sqrt(n)

    因为可以证明在取整情况下也满足  x=n /a1/a2/a3/……/ak  =  n/(a1*a2*a3*……*ak)   即x=n/m   而n/m在取整的情况下 就2*sqrt(n)种不同的取值.

    接着因为我们预处理了n^(2/3) 即106内的结果, 我们可以再扔掉一部分状态,只留n^(1/3)的状态用公式计算出来就行了。

    到这里,因为每次转移复杂度是2*sqrt(x)的,可以证明出杜教筛的复杂度是

    ※提取要计算的状态

    好废话讲了那么多,我们先预处理出,所要计算的n^(1/3)的状态的位置(即(n/1,n/2,n/3,……n/(1e3)))

     这里直接用暴力算出来就行,代码如下,注意要按大小顺序存起来方便后面转移

    1    vector<ll>q;
    2     for(int i=1; i<=n; i++)
    3     {
    4         i=n/(n/i);
    5         if(n/i<=UB)
    6             break;
    7         q.push_back(n/i);
    8     }
    从小到大提取要计算的状态

    ※构造完美哈希算法

    这里只要一行代码,就可以构造出完美哈希从而扔掉哈希表,然后把s数组多开大sqrt(n)的大小就行了,其中的原理和精髓可看最终代码

    #define id(x) x<=UB?x:(n/(x)+UB)

    注:这里可以证明若UB>sqrt(n),对于所有大于UB的x。n/x 的取值一对一映射到[1,n/UB]  区间内

    ※最终的实现方式

    最后总体代码如下

     1 ll cal(ll n)
     2 {
     3     vector<ll>q;
     4     for(int i=1; i<=n; i++)
     5     {
     6         i=n/(n/i);
     7         if(n/i<=UB)
     8             break;
     9         q.push_back(n/i);
    10     }
    11     int k;
    12     ll ans,i,j,m,t;
    13     for(k=q.size()-1; k>=0; k--)///从小到大枚举状态
    14     {
    15         ans=0;
    16         m=q[k];
    17         for(i=2; i<=m; i++)///递推公式,和文章上面哈希表的写法对比一下就知道写的是什么了。
    18         {
    19             j=i;
    20             t=(m/i);
    21             i=m/t;
    22             ans+=(i-j+1)*s[id(t)];
    23         }
    24         ans=(h(m)-ans);
    25         s[id(m)]=ans;
    26     }
    27     return s[id(n)];
    28 }
    非递归+完美哈希

    使用步骤四:验板子

    你要想要知道自己板子有没写错,或者写搓怎么办?。上OJ验板子, 一直wa也不知道wa在哪,题目函数太复杂怎么办?

    答:这里要用验筛法板子的神级函数,f(i)=i. 这个函数前n项和对1e9+7 取模的结果,用脚指头算都知道是 n*(n+1)/2%(1e9+7) ,很容易知道你板子算出来的结果对不对。中间过程也比较好处理。

    1. 首先步骤一:求s[i]前106项,这里我就不详细说明了,s[i]=s[i-1]+i
    2. 接着步骤二:写求h的函数,,emmmmmmmmmmmm这东西能快速算???。  这里有一个只有s(n)很快速计算第n项时(要是能快速计算还写毛杜教筛,所以这个技巧没卵用,不过能用来验板子)才能使用的技巧——枚举d的取值,则有,在配合数论分块就能在根号复杂度内算出h(n)了
    3. 套上面步骤三写好的代码,注意加个取模
    4. 运行代码,从文件中读入1000000000。看程序的运行时间和计算结果

    示例代码如下:

     1 #include<stdio.h>
     2 #include<string.h>
     3 #include<vector>
     4 #include<algorithm>
     5 using namespace std;
     6 #define ll long long
     7 #define id(x) x<=UB?x:(n/(x)+UB)
     8 const int UB=1e6;
     9 const int N=100000;
    10 const ll mod=1e9+7;
    11 ll s[UB+N];
    12 void init()///预处理s的前UB项
    13 {
    14     for(int i=1;i<=UB;i++)
    15     {
    16         s[i]=s[i-1]+i;
    17         s[i]%=mod;
    18     }
    19 }
    20 ll h(ll n)///h=Σ Σf(d)*[i%d==0]  h为狄利克雷卷积的前缀和
    21 {
    22     ll i,j;
    23     ll sum=0;
    24     for(i=1;i<=n;i++)
    25     {
    26         j=i;
    27         i=n/(n/i);
    28         sum+=(j+i)*(i-j+1)/2%mod*(n/i)%mod;
    29     }
    30     return sum%mod;
    31 }
    32 ll cal(ll n)
    33 {
    34     vector<ll>q;
    35     for(int i=1; i<=n; i++)
    36     {
    37         i=n/(n/i);
    38         if(n/i<=UB)
    39             break;
    40         q.push_back(n/i);
    41     }
    42     int k;
    43     ll ans,i,j,m,t;
    44     for(k=q.size()-1; k>=0; k--)///从小到大枚举状态
    45     {
    46         ans=0;
    47         m=q[k];
    48         for(i=2; i<=m; i++)///递推公式,和文章上面哈希表的写法对比一下就知道写的是什么了。
    49         {
    50             j=i;
    51             t=(m/i);
    52             i=m/t;
    53             ans+=(i-j+1)*s[id(t)]%mod;
    54         }
    55         ans=(h(m)-ans)%mod;
    56         if(ans<0)
    57             ans+=mod;
    58         s[id(m)]=ans;
    59     }
    60     return s[id(n)];
    61 }
    62 int main()
    63 {
    64     int i,j;
    65     ll n=1e9;
    66    // scanf("%lld",&n);
    67     init();
    68     printf("out=%lld,ans=%lld
    ",cal(n),(n*(n+1)/2)%mod);
    69     return 0;
    70 }
    测板子示例

     这个这个必须一秒内出来,且答案正确,不然你就凉拉

  • 相关阅读:
    python之访问限制机制
    python之property装饰器
    python之封装、组合
    python中classmethod和staticmethod
    (专题一)01 matlab基础
    代数运算
    点运算
    研究生学习安排2019/6/6
    图像处理中创建CDib类时无法选择基类类型时怎么办
    04 学习java养成良好的写作习惯
  • 原文地址:https://www.cnblogs.com/qswg/p/9663358.html
Copyright © 2011-2022 走看看