从零开始学杜教筛
实际上略过了一小部分基础数论。
杜教筛时间复杂度的证明还需要一点微积分基础。
积性函数
数论函数是定义域在整数上的函数,其中积性函数因为其具有十分朴实且优美的性质,成为了 OI 数论中不可或缺的基础。
定义
设数论函数
特别的,若对于任意
常见的积性函数有:
- 单位函数:
(完全积性) - 恒等函数:
(完全积性),当 简记为 - 常数函数:
(完全积性) - 除数函数:
, 即因数个数,简记为 ; 即因子之和,简记为 - 欧拉函数:
,即 以内与 互质的数的个数。 - 莫比乌斯函数:
同时有两个重要的推论:
- 积性函数
一定满足 ,因为 。 - 通过所有质数处的点值可以唯一确定完全积性函数;通过全部
处点值可以唯一确定积性函数,由于唯一分解定理。
狄利克雷卷积
定义
对于两个数论函数
性质
狄利克雷卷积有很多优良的性质:
- 交换律:
- 结合律:
- 单位元:取
,则对任意数论函数
都有 - 两个积性函数的狄利克雷卷积仍然是积性函数。
狄利克雷逆
已知数论函数
应用
这个东西有啥用呢?我们不妨试着找找
记
设
- 在
处 - 在
处 - 在
处
这些条件看上去都是必要的,但是并没有充分性。
根据狄利克雷卷积的性质,由于
事实上,
我们考虑另一个狄利克雷卷积:
记
设
- 在
处 - 在
处
归纳得出:
事实上,
恒等式
我们有以下狄利克雷卷积恒等式:
其中前三个恒等式非常重要,在杜教筛式子的推导中也会出现。
数论分块(整除分块)
这是几乎所有数论题都会用到的计算方法,非常重要。一切来源于一个精巧的观察。
重要观察
对一个正整数
证明:当
枚举取值
现在我们有一个
设
我们可以不断地求出
线性筛
原理
顾名思义,线性筛可以在严格线性时间复杂度筛质数或各种积性函数。其原理是用每个数的最小质因子来筛去它。
正确性
具体实现是维护从小到大的质数
复杂度
复杂度如何保证?对于每个数去筛都有一次终止,即
代码实现
贴一份线性筛代码,其实很简短。
线性筛 和
void init() { mu[1] = 1, phi[1] = 1; for(int i = 2; i <= maxv; i++) { if(isp[i]) { primes.push_back(i); mu[i] = -1, phi[i] = i - 1; } for(int p : primes) { if(i * p > maxv) break; isp[i * p] = false; if(i % p) { mu[i * p] = -mu[i], phi[i * p] = phi[i] * (p - 1); } else { mu[i * p] = 0, phi[i * p] = phi[i] * p; break; } } } return; }
杜教筛
简介
对于数论函数
式子推导
找另一个恰当的数论函数
为了避免枚举因数,我们更换指标:用新的
把对
(以上部分也可以用于在其他时候处理狄利克雷卷积)
接下来我们处理
令
如果
时间复杂度
令
我们认为求
设计算一次
若我们可以预处理一部分
若使用线性筛预处理前缀和,则
最后,对于多组询问要使用记忆化,防止重复求
应用
筛
我们有恒等式
启发我们取
即:
筛
我们有恒等式
启发我们取
即:
代码实现
Luogu P4213 【模板】杜教筛 Link
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int maxn = (1 << 21) + 10, maxv = (1 << 21); ll T, n; vector<ll> primes, isp(maxn, true); ll mu[maxn], phi[maxn]; unordered_map<int, ll> sphi; unordered_map<int, int> smu; void init() { mu[1] = 1, phi[1] = 1; for(int i = 2; i <= maxv; i++) { if(isp[i]) { primes.push_back(i); mu[i] = -1, phi[i] = i - 1; } for(int p : primes) { if(i * p > maxv) break; isp[i * p] = false; if(i % p) { mu[i * p] = -mu[i], phi[i * p] = phi[i] * (p - 1); } else { mu[i * p] = 0, phi[i * p] = phi[i] * p; break; } } } for(int i = 1; i <= maxv; i++) mu[i] += mu[i - 1], phi[i] += phi[i - 1]; return; } ll get_phi(int x) { if(x <= maxv) return phi[x]; if(sphi[x]) return sphi[x]; ll res = 1ll * x * (1ll * x + 1) / 2; for(ll l = 2, r = 0; l <= x; l = r + 1) { r = x / (x / l); res -= get_phi(x / l) * (r - l + 1); } return sphi[x] = res; } int get_mu(int x) { if(x <= maxv) return mu[x]; if(smu[x]) return smu[x]; ll res = 1; for(ll l = 2, r = 0; l <= x; l = r + 1) { r = x / (x / l); res -= get_mu(x / l) * (r - l + 1); } return smu[x] = res; } int main() { ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0); init(); cin >> T; while(T--) { cin >> n; cout << get_phi(n) << " " << get_mu(n) << endl; } return 0; }
完结撒花 😃
本文作者:Ydoc770
本文链接:https://www.cnblogs.com/Ydoc770/p/18703321
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步