杜教篩使用心得


現在杜教篩好像是個人都會了 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\n",cal(n),(n*(n+1)/2)%mod);
69     return 0;
70 }
測板子示例

 這個這個必須一秒內出來,且答案正確,不然你就涼拉


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM