杜教篩模板
杜教篩是用來干蛤的呢?
它可以在非線性時間內求積性函數前綴和。
前置知識
積性函數
積性函數:對於任意互質的整數 \(a,b\) 有 \(f(ab)=f(a)f(b)\) 則稱 \(f(x)\) 的數論函數。
完全積性函數:對於任意整數 \(a,b\) 有 \(f(ab)=f(a)f(b)\) 的數論函數。
- 常見的積性函數:\(\varphi,\mu,\sigma,d\)
- 常見的完全積性函數:\(\epsilon,I,id\)
這里特殊解釋一下 \(\epsilon,I,id\) 分別是什么意思:
\(\epsilon(n) = [n=1], I(n) = 1, id(n) = n\)
狄利克雷卷積
設 \(f, g\) 是兩個數論函數,它們的狄利克雷卷積卷積是:\((f*g)(n) = \sum \limits _{d | n} f(d) g(\frac{n}{d})\)
性質:滿足交換律,結合律
單位元:\(\epsilon\) (即 \(f*\epsilon=f\))
結合狄利克雷卷積得到的幾個性質:
- \(\mu * I = \epsilon\)
- \(\varphi * I = id\)
- \(\mu * id = \varphi\)
莫比烏斯反演
若
則
證明:這里需要用到前面提到的性質:\(\mu * I = \epsilon\)
給出的條件等價於 \(g=f * I\)
所以 \(g*\mu=f*I*\mu=f*\epsilon=f\) 即 \(g * \mu = f\) 即 結論。
杜教篩
設現在要求積性函數 \(f\) 的前綴和, 設 \(\sum \limits_{i=1}^{n} f(i) = S(n)\)。
再找一個積性函數 \(g\) ,則考慮它們的狄利克雷卷積的前綴和
其中得到第一行是根據狄利克雷卷積的定義。
得到第二行則是先枚舉 \(d\) 提出 \(g\) 。
得到第三行則是把 $\sum\limits _{i=1}^{\lfloor \frac{n}{d}\rfloor } f(i) $ 替換為 $S(\lfloor \frac{n}{d} \rfloor) $
接着考慮 \(g(1)S(n)\) 等於什么。
可以發現,他就等於
(可以理解成從1開始的前綴和減去從2開始的前綴和就是第一項)
前面這個式子\(\sum \limits _{i=1}^{n} g(i) S(\lfloor \frac{n}{i} \rfloor)\) ,根據剛才的推導,他就等於 \(\sum\limits_{i=1}^{n}(f*g)(i)\)
所以得到杜教篩的核心式子:
得到這個式子之后有什么用呢?
現在如果可以找到一個合適的積性函數 \(g\) ,使得可以快速算出 \(\sum\limits_{i=1}^{n}(f*g)(i)\) 和 \(g\) 的前綴和,便可以用數論分塊遞歸地求解。
代碼按照理解大概可以寫成這樣(默認 ll
為 long long
)
(可以理解成一個偽代碼。。就是一個思路的框架)
ll GetSum(int n) { // 算 f 前綴和的函數
ll ans = f_g_sum(n); // 算 f * g 的前綴和
// 以下這個 for 循環是數論分塊
for(ll l = 2, r; l <= n; l = r + 1) { // 注意從 2 開始
r = (n / (n / l));
ans -= (g_sum(r) - g_sum(l - 1)) * GetSum(n / l);
// g_sum 是 g 的前綴和
// 遞歸 GetSum 求解
} return ans;
}
這個代碼的復雜度是 \(O(n^{\frac{3}{4}})\),證明如下:
設求出 \(S(n)\) 的復雜度是 \(T(n)\) ,要求出 \(S(n)\) 需要求出 \(\sqrt n\) 個 \(S (\lfloor \frac{n}{i} \rfloor)\) 的值,結合數論分塊的復雜度 \(O(\sqrt n)\) 可得:
還可以進一步優化杜教篩,即先線性篩出前 \(m\) 個答案,之后再用杜教篩。這個優化之后的復雜度是:
當 \(m = n ^ {\frac{2}{3}}\) 時,\(T(n) = O(n^{\frac{2}{3}})\)
可以使用哈希表來存下已經求過的答案,也可以不用。
考慮到上面的求和過程中出現的都是 $\lfloor \frac{n}{i} \rfloor $ 。開一個大小為兩倍 \(\sqrt n\) 的數組 \(dp\) 記錄答案。如果現在需要求出 GetSum(x)
,若 \(x \leq \sqrt n\) ,返回 dp[x]
,否則返回 dp[sqrt n + n / i]
即可。這樣可以省去哈希表的復雜度。
實戰演練
再掛一次核(tao)心(lu)式,全都要靠它:
它的關鍵就是要找到合適的 \(g\) 使得這個東西可以快速地算。
理論知識大概就這么多,接下來看幾個例子:
(1) \(\mu\) 的前綴和
考慮到莫比烏斯函數的性質 \(\mu * I = \epsilon\) ,自然想到取 \(f=\mu,g=I,f*g=\epsilon\) 。
其中 \(I\) 的前綴和和 \(\epsilon\) 的前綴和都弱到爆了。。
所以就輕松的解決了。
杜教篩代碼:
inline ll GetSumu(int n) {
if(n <= N) return sumu[n]; // sumu是提前篩好的前綴和
if(Smu[n]) return Smu[n]; // 記憶化
ll ret = 1ll; // 單位元的前綴和就是 1
for(int l = 2, r; l <= n; l = r + 1) {
r = n / (n / l); ret -= (r - l + 1) * GetSumu(n / l);
// (r - l + 1) 就是 I 在 [l, r] 的和
} return Smu[n] = ret; // 記憶化
}
(2) \(\varphi\) 的前綴和
考慮到 \(\varphi\) 的性質 \(\varphi * I = id\),取 \(f = \varphi, g = I, f * g = id\)
\(f * g\) 即 \(id\) 的前綴和為 \(\frac{n * (n+1)}{2}\)
杜教篩代碼:
inline ll GetSphi(int n) {
if(n <= N) return sump[n]; // 提前篩好的
if(Sphi[n]) return Sphi[n]; // 記憶化
ll ret = 1ll * n * (n + 1) / 2; // f * g = id 的前綴和
for(int l = 2, r; l <= n; l = r + 1) {
r = n / (n / l); ret -= (r - l + 1) * GetSphi(n / l);
// 同上,因為兩個的 g 都是 I
} return Sphi[n] = ret; // 記憶化
}
(1) & (2) 就是杜教篩模板 luogu p4213
(3) (綜合)\(\sum\limits_{i=1}^{n}\varphi(i) \cdot i\)
令 \(f = \varphi \cdot id, g = id\), 考慮迪利克雷卷積的形式得到 \((f*g)(n)=\sum \limits _{d|n} (\varphi(d) \cdot d) \cdot (\frac{n}{d}) = n \sum\limits_{d|n}\varphi(d)=n^2\)
即 \((f * g)(i) = i^2\)
這樣就可以快速求得 \((f*g)(i)\) 的前綴和 \(\frac{n(n+1)(2n+1)}{6}\)
就可以了。