介绍
我们在 OI 题目中,不难会遇到给定一个积性函数 ,求
的题目。虽然对于积性函数我们已经有成熟的线性筛方法来说可以做到 ,可是有的出题人就是毒瘤,可是有的题目的数据范围要求给出一个复杂度小于线性的做法,这时候神仙就搞出了杜教筛这一东西。
前置芝士
芝士1:线性筛
首先我们知道,基本上所有的积性函数都可以线性筛。
积性函数的特点:
如果我们想线性筛积性函数,首先这个过程一定是建立在欧拉筛素数上的,然后我们考虑一些性质:
- 当 为质数时, 的取值
- 当这个数不是第一次被筛到的时候,取值的特性
我们拿 举例:
什么你还不知道 是什么?快去看看莫比乌斯反演吧。
首先发现 ,。
然后发现当 能不止被一种乘积筛出来的时候,。
如果是被最小的质因数筛出的时候,就可以依据积性函数的定义来更新了。
直接放一个线性筛 和 的代码吧。
inline void pre(int n=MAXN-5){ phi[1] = mu[1] = 1; FOR(i,2,n){ if(!p[i]){ prime[++cnt] = i; mu[i] = -1;phi[i] = i-1; } for(Re int j = 1;j <= cnt && i*prime[j] <= n;++j){ p[i*prime[j]] = true; if(i%prime[j]){ phi[i*prime[j]] = phi[i]*phi[prime[j]]; mu[i*prime[j]] = -mu[i]; } else{ phi[i*prime[j]] = phi[i]*prime[j]; mu[i*prime[j]] = 0; break; } } } }
芝士2:常见积性函数
纯粹是列举一下防止后面因为符号不同的问题而看不懂。 ,莫比乌斯函数。 ,欧拉函数。 ,约数个数,即 。 ,约数和,即 。
完全积性函数:
:常函数 。 :狄利克雷卷积单位元。 :单位函数 。
如果你学过莫比乌斯反演,应该已经掌握了所有的前置芝士了,现在我们进入正题。
杜教筛过程
下文中定义狄利克雷卷积的运算符号为
我们现在要对于一个积性函数 ,求
考虑找到一个合适的函数 ,使得 的前缀和易于计算。
我们考虑 的前缀和,并加以推导,得
\begin{align}
&\sum_{i=1}^n (fg)(i) \
&= \sum_{i=1}^n \sum_{d|i} g(i)f(\frac{n}{i}) \
&= \sum_{d=1}^n g(d) \sum_{i=1}^{\lfloor \frac{n}{d} \rfloor} f(i) \
&= \sum_{i=1}^n g(i) S(\lfloor \frac{n}{i}\rfloor) \
&= g(1)S(n) + \sum_{i=2}^n g(i)S(\lfloor \frac{n}{i}\rfloor)
\end{align*}
考虑进行一些小的移项,最后可得我们一般使用的杜教筛的式子:
如果我们对于 的前缀和有快速求法(e.g. ),那么预处理出 函数的前 ,后面数论分块+递归求解,就可以做到 的优秀复杂度内求得 。
现在我们来尝试用杜教筛来解决求一些函数的前缀和问题。
BZOJ 3944 Sum
题目链接
题目要让你求 和 ,其中 。
首先考虑 :
首先我们需要找到一个函数,使得它与 的狄利克雷卷积的前缀和是容易计算的。注意到关于 的一个性质:。
发现 的前缀和十分好算(就是 ),并且 非常有利于乘法分块,于是我们取 ,代入上式,得:
类比我们来考虑 :
发现一个性质:
的前缀和可以用等差数列求和公式求得,代入上式可以发现:
也就是
现在我们来证明一下这东西的复杂度是什么 。
设 表示计算出 的复杂度,可以得到:
展开一层就可以了,因为往下都是高阶小量可以不考虑。
我们可以用筛法处理前 个,如果 ,则
一般我们 取 ,可以得到复杂度为 的算法。
#include <tr1/unordered_map> #include <algorithm> #include <iostream> #include <cstring> #include <climits> #include <cstdlib> #include <cstdio> #include <vector> #include <cmath> #include <ctime> #include <queue> #include <stack> #include <map> #include <set> #define fi first #define se second #define U unsigned #define P std::pair #define Re register #define LL long long #define pb push_back #define MP std::make_pair #define all(x) x.begin(),x.end() #define CLR(i,a) memset(i,a,sizeof(i)) #define FOR(i,a,b) for(Re int i = a;i <= b;++i) #define ROF(i,a,b) for(Re int i = a;i >= b;--i) #define DEBUG(x) std::cerr << #x << '=' << x << std::endl using namespace std::tr1; const int MAXN = 6000000+5; bool p[MAXN]; int mu[MAXN],phi[MAXN],prime[10001],cnt; LL sum1[MAXN];int sum2[MAXN]; inline void pre(int n=MAXN-5){ phi[1] = mu[1] = 1; FOR(i,2,n){ if(!p[i]){ prime[++cnt] = i; mu[i] = -1;phi[i] = i-1; } for(Re int j = 1;j <= cnt && i*prime[j] <= n;++j){ p[i*prime[j]] = true; if(i%prime[j]){ phi[i*prime[j]] = phi[i]*phi[prime[j]]; mu[i*prime[j]] = -mu[i]; } else{ phi[i*prime[j]] = phi[i]*prime[j]; mu[i*prime[j]] = 0; break; } } } FOR(i,1,n) sum1[i] = sum1[i-1]+phi[i],sum2[i] = sum2[i-1]+mu[i]; } unordered_map<LL,LL> S1; unordered_map<int,int> S2; inline LL calc1(LL x){ if(x <= MAXN-5) return sum1[x]; if(S1[x]) return S1[x]; LL ans = x*(x+1)/2; for(Re LL l=2,r;l <= x;l = r+1){ r = x/(x/l); ans -= calc1(x/l)*(r-l+1); } return S1[x] = ans; } inline int calc2(int x){ if(x <= MAXN-5) return sum2[x]; if(S2[x]) return S2[x]; int ans = 1; for(Re int l = 2,r;l >= 0 && l <= x;l = r+1){ r = x/(x/l); ans -= calc2(x/l)*(r-l+1); } return S2[x] = ans; } inline void Solve(){ int n;scanf("%d",&n); printf("%lld %d\n",calc1(n),calc2(n)); } int main(){ pre(); int T;scanf("%d",&T); while(T--) Solve(); return 0; }
两道小题目
Sum 加强版
求上式的值,其中 是线性筛跑不过的量级。
我们一眼看不出来 用什么好,不妨先假定有了这个函数并代入狄利克雷卷积的形式:。
这个常数 d 太烦人了,不如我们想办法消去它。我们尝试取 ,代入化简得到
这样的话前缀和就很好求了,带回原式可得:
大家都会 求 ,也就是
Hdu 5608 Function
题目链接
告诉你一个数论函数 ,满足
多次询问 的前缀和。 ,只有五组 。
长得十分像莫比乌斯反演的经典形式。
我们设 ,原式转化为
反演一波可以得到:
我们要求的答案变成了:
进行一些微小的变换,可以推导出
\begin{align}
& \sum_{i=1}^n \sum_{d|n} g(d) \mu(\frac{n}{d})\
&= \sum_{d=1}^n g(d) \sum_{i=1}^{\lfloor \frac{n}{d} \rfloor} \mu(i)
\end{align}
发现 的前缀和十分好求,这样就可以通过数论分块+杜教筛 来做了。
#include <unordered_map> #include <algorithm> #include <iostream> #include <cstring> #include <climits> #include <cstdlib> #include <cstdio> #include <vector> #include <cmath> #include <ctime> #include <queue> #include <stack> #include <map> #include <set> #define fi first #define se second #define U unsigned #define P std::pair #define Re register #define LL long long #define pb push_back #define MP std::make_pair #define all(x) x.begin(),x.end() #define CLR(i,a) memset(i,a,sizeof(i)) #define FOR(i,a,b) for(Re int i = a;i <= b;++i) #define ROF(i,a,b) for(Re int i = a;i >= b;--i) #define DEBUG(x) std::cerr << #x << '=' << x << std::endl const int MAXN = 2000000+5; const int ha = 1e9 + 7; bool p[MAXN]; int prime[MAXN],mu[MAXN],cnt; inline int qpow(int a,int n=ha-2){ int res = 1; while(n){ if(n & 1) res = 1ll*res*a%ha; a = 1ll*a*a%ha; n >>= 1; } return res; } inline void pre(int n=MAXN-5){ mu[1] = 1;p[1] = 1; FOR(i,2,n){ if(!p[i]) prime[++cnt] = i,mu[i] = -1; for(int j = 1;j <= cnt && i*prime[j] <= n;++j){ p[i*prime[j]] = true; if(i%prime[j]) mu[i*prime[j]] = -mu[i]; else{ mu[i*prime[j]] = 0; break; } } } FOR(i,1,n) mu[i] = (mu[i-1]+mu[i]+ha)%ha; } std::unordered_map<int,int> S; inline int calc(int x){ if(x <= MAXN-10) return mu[x]; if(S[x]) return S[x]; int ans = 1; for(int l = 2,r;l > 0 && l <= x;l = r+1){ r = x/(x/l); ans = (ans-1ll*calc(x/l)*(r-l+1)%ha+ha)%ha; } return S[x] = ans; } int inv6 = qpow(6),inv2 = qpow(2); inline int get(LL x){ int res = 1ll*x*(x+1)%ha*(2*x+1)%ha; res = 1ll*res*inv6%ha; res = (1ll*res+2ll*x)%ha; int t = 3ll*x%ha*(x+1)%ha*inv2%ha; res = (res-t+ha)%ha; return res; } inline int get(int l,int r){ return (get(r)-get(l-1)+ha)%ha; } inline void Solve(){ int ans = 0,x;scanf("%d",&x); for(int l = 1,r;l <= x;l = r+1){ r = x/(x/l); int t = 1ll*calc(x/l)*get(l,r)%ha; // DEBUG(l);DEBUG(r);DEBUG(t); ans = ((ans+t)%ha+ha)%ha; } printf("%d\n",ans); } int main(){ pre(); int T;scanf("%d",&T); while(T--) Solve(); return 0; }