初等数论学习笔记 III:数论函数与筛法
1. 数论函数
本篇笔记所有内容均与数论函数相关。因此充分了解各种数论函数的名称,定义,符号和性质是必要的。
1.1 相关定义
- 数论函数:定义域为正整数的函数称为 数论函数。因其在所有正整数处均有定义,故可视作数列。OI 中常见的数论函数的陪域(即可能的取值范围)为整数。
- 加性函数:若对于任意
且 均有 ,则称 为 加性函数。注意区分代数中的加性函数。 - 积性函数:若对于任意
且 均有 ,则称 为 积性函数。易知 是必要条件。 - 完全积性函数:若对于任意
均有 ,则称 为 完全积性函数。完全积性函数一定是积性函数。 - 数论函数的 加法:对于数论函数
, 表示 和 对应位置相加,即 。 - 数论函数的 数乘:对于数
和数论函数 , 表示 的各个位置乘 ,即 。一般简记作 。 - 数论函数的 点乘:对于数论函数
, 表示 和 对应位置相乘,即 。为与狄利克雷卷积符号 作区分,点乘符号通常不省略。
1.2 常见数论函数
设
- 单位函数:
。当 时取值为 ,否则取值为 。它是完全积性函数。 - 常数函数:
。它是完全积性函数。 - 恒等函数:
。 记作 。它是完全积性函数。 - 除数函数:
。 表示 的约数个数,记作 或 。 表示 的约数和,记作 。 有计算式 :根据乘法分配律, 的所有约数的 次方之和可写作 ,等比数列求和后即得。 - 欧拉函数:
,表示 以内与 互质的数的个数。关于欧拉函数的性质,详见笔记 I. - 本质不同质因子个数函数:
,表示 的本质不同质因子个数。 - 莫比乌斯函数:
。
以上所有函数除
2. 素数筛法
素数筛法是数论体系中最基本的知识点,几乎所有数论题目都需要筛出
2.1 埃氏筛素数
埃氏筛用所有已经筛出的素数的倍数标记合数:从
for(int i = 2; i < N; i++)
if(!vis[i]) {
pr[++cnt] = i;
for(int j = 2 * i; j < N; j += i) vis[j] = 1;
}
常数优化:根据合数
for(int i = 2; i < N; i++)
if(!vis[i]) {
pr[++cnt] = i;
if(1ll * i * i < N) // 防止 i * i 溢出导致 RE
for(int j = i * i; j < N; j += i) vis[j] = 1;
}
埃氏筛的精髓在于其复杂度证明:不超过
这说明埃氏筛的复杂度为
证明来自戴江齐学长:
因为每个数只会被其素因子筛到,所以
根据
根据
两边同时取对数,
2.2 线性筛素数
线性筛也称欧拉筛,它和埃氏筛的思想类似。
埃氏筛的优秀之处在于仅用质数的倍数筛去合数,但合数会被多个质因子筛到。让每个合数仅被筛一次就能做到线性。
考虑用每个合数的 最小质因子 筛去它:从
综上,有如下步骤:
- 从小到大遍历当前筛出的所有素数
,将 标记为合数。 - 若
,退出循环。因为 ,所以 的最小质因子为 不是 ,再筛就重复了。
时间复杂度线性。模板题 P3383 代码如下:
#include <bits/stdc++.h>
using namespace std;
constexpr int N = 1e8 + 5;
bool vis[N];
int n, q, pr[N / 16], cnt;
int main() {
cin >> n;
for(int i = 2; i <= n; i++) {
if(!vis[i]) pr[++cnt] = i;
for(int j = 1; j <= cnt && i * pr[j] <= n; j++) {
vis[i * pr[j]] = 1;
if(i % pr[j] == 0) break;
}
}
cin >> q;
while(q--) {
int x;
scanf("%d", &x);
printf("%d\n", pr[x]);
}
return 0;
}
2.3 线性筛积性函数
线性筛提供了在线性时间内筛出具有特殊性质的积性函数在
只要积性函数
- 注意,这只是
可线性筛的 充分但不必要 条件。存在更弱的条件使得 可线性筛但并不常见,如 计算 ,这将在第三章介绍。
根据积性函数的性质,只要预先求出
for(int i = 2; i < N; i++) {
if(!vis[i]) pr[++pcnt] = i, f[i] = ..., low[i] = i; // 单独算 f(p)
for(int j = 1; j <= pcnt && i * pr[j] < N; j++) {
vis[i * pr[j]] = 1;
if(i % pr[j] == 0) { // i 与 p 不互质
low[i * pr[j]] = low[i] * pr[j];
if(i == low[i]) f[i * pr[j]] = ...; // i = p ^ k,单独算 f(p ^ {k + 1})
else f[i * pr[j]] = f[i / low[i]] * f[low[i * pr[j]]];
break;
}
low[i * pr[j]] = pr[j];
f[i * pr[j]] = f[i] * f[pr[j]]; // i 与 p 互质,f(ip) = f(i)f(p)
}
}
3. 狄利克雷卷积
狄利克雷(Dirichlet)卷积是数论函数的基本运算,其重要程度相当于代数中的四则运算。
3.1 定义与性质
为数列定义卷积,自然想到加法卷积
上式简记为
接下来证明一些狄利克雷卷积的性质。
性质 1:狄利克雷卷积具有 交换律,结合律,分配律。
交换律:
其中
结合律:
其中
分配律:
因此
性质 2:
。
证明:
因此单位函数
既然存在单位元,就可以据此定义数论函数
性质 3:
可逆当且仅当 。
证明:设
过
性质 4:
的充要条件是 ,其中 。
证明:
性质 5:积性函数的狄利克雷卷积是积性函数。
证明:考虑积性函数
其中
性质 6:积性函数的逆元是积性函数。
证明:设
考虑归纳法。对于
因为
综合性质 5 和性质 6,两个积性函数的积与商都是积性函数。注意,积性函数的和与差不是积性函数。
3.2 线性筛 Dirichlet 卷积
根据积性函数的狄利克雷卷积是积性函数这一结论,我们尝试在线性时间内筛出
写出
对于第一和第三种情况,线性筛时可以总代价
- 特别的,当
为完全积性函数时, 可以写作 ,可以 方便地计算。对于 同理。
尝试估计第二部分的复杂度。考虑到所有小于
感性理解,
当
因此,
综上,使用线性筛求出两个在质数幂处取值已知的积性函数的狄利克雷卷积在
我们得到了积性函数可线性筛的更弱的条件:可以
3.3 狄利克雷前缀和
前置知识:高维前缀和。
任意数论函数
对每个
将每个数
根据高维前缀和的求法,枚举每一维并将所有下标关于该维做前缀和,可得狄利克雷前缀和的实现方法:初始令
根据小于
模板题 P5495 代码。
#include <bits/stdc++.h>
using namespace std;
constexpr int N = 2e7 + 5;
int n;
unsigned ans, a[N], seed;
inline unsigned rd() {
seed ^= seed << 13, seed ^= seed >> 17, seed ^= seed << 5;
return seed;
}
bool vis[N];
int cnt, pr[N >> 3];
void sieve() {
for(int i = 2; i <= n; i++) {
if(!vis[i]) pr[++cnt] = i;
for(int j = 1; j <= cnt && i * pr[j] <= n; j++) {
vis[i * pr[j]] = 1;
if(i % pr[j] == 0) break;
}
}
}
int main() {
cin >> n >> seed, sieve();
for(int i = 1; i <= n; i++) a[i] = rd();
for(int i = 1; i <= cnt; i++)
for(int j = 1; j * pr[i] <= n; j++)
a[j * pr[i]] += a[j];
for(int i = 1; i <= n; i++) ans ^= a[i];
cout << ans << endl;
return 0;
}
4. 数论分块
数论分块又称整除分块,因其解决的问题与整除密切相关而得名。数论分块用于求解形如
的和式。前提为
感性认知:使得
4.1 算法介绍
如果
当
结论 1:对于任意
,不同的 至多 个。
证明:
根据结论 1,枚举
这样,问题转化为求使得
如何不重不漏地枚举所有整除值?没有必要。我们只需依次枚举每个
具体地,令当前枚举到的
每个整除值仅会被遍历一次,时间复杂度
- 注意,当
的上界不等于 时,设其为 ,则 应与 取较小值(处理 的情况),且当 时需要特判,直接令 等于 (处理 的情况)。
4.2 扩展
4.2.1 向上取整
尝试将向下取整变为向上取整。
对于左边界
第三步转换是因为
注意特判
4.2.2 高维数论分块
当和式中出现若干下取整,形如
时,只需稍作修改,令
时间复杂度为
4.3 例题
I. [模拟赛] 你还没有卸载吗
给定
,求有多少 使得 。 ,其他所有数 。时限 1s。
考虑数论分块
另一种方法是直接二维数论分块。细节更少,且时间复杂度相同。
#include <bits/stdc++.h>
using namespace std;
int T, a1, b1, a2, b2, n;
int main() {
cin >> T;
while(T--) {
int ans = 0;
cin >> a1 >> b1 >> a2 >> b2 >> n;
for(int l = 1, r = 1; l <= n; l = r + 1) {
r = min(n, min(a1 / l ? a1 / (a1 / l) : n, a2 / l ? a2 / (a2 / l) : n));
if(b1 + a1 / l == b2 + a2 / l) ans += r - l + 1;
}
cout << ans << endl;
}
return 0;
}
*II. CF1603C Extreme Extension
数论分块优化 DP。
一个数如何分裂由后面分裂出来的数的最小值决定,显然贪心使分出来的数尽量均匀,例如若
从后往前考虑,对于每个数
首先明确两点:
分裂成若干 的数,最少分裂次数为 ,分裂成 个数。 分裂成 个数,这些数最小值的最大值为 。
考虑转移。
注意到对于固定的分裂次数,分裂出的值也是确定的。考虑枚举使得分裂次数相同的区间
令
考虑在每个位置处统计该位置在所有子段中总分裂次数之和,则答案加上
注意,当
用 vector
存储所有
III. P2260 [清华集训2012] 模积和
求
原式可写作
全部使用数论分块解决。可能需要的公式:
时间复杂度
*IV. P3579 [POI2014] PAN-Solar Panels
非常不错的题目。
当
我们当然可以四维数论分块,但注意到在使得
时间复杂度
5. 莫比乌斯函数
前置知识:容斥原理。
到达数论最高城,莫比乌斯反演!太好用啦莫反,哎呀这不 GCD 么,还是枚举倍数吧家人们。
5.1 引入
观察
设
由于积性函数的逆元具有积性,所以
。 。 。
据此,可归纳证明
考虑
验证:令
也可以从容斥系数的角度理解莫比乌斯函数。设
换言之,对
5.2 筛
据定义,线性筛莫比乌斯反演是容易的。
int vis[N], cnt, pr[N], mu[N];
void sieve() {
mu[1] = 1;
for(int i = 2; i < N; i++) {
if(!vis[i]) pr[++cnt] = i, mu[i] = -1;
for(int j = 1; j <= cnt && i * pr[j] < N; j++) {
vis[i * pr[j]] = 1;
if(i % pr[j] == 0) break; // 此时 i * pr[j] 含至少两个 pr[j],mu = 0
mu[i * pr[j]] = -mu[i]; // mu[i * pr[j]] = mu[i] * mu[pr[j]] = -mu[i]
}
}
}
当时间复杂度可接受时,根据
int mu[N];
void sieve() {
mu[1] = 1;
for(int i = 1; i < N; i++)
for(int j = i + i; j < N; j += i)
mu[j] -= mu[i];
}
5.3 莫比乌斯反演
- 用和式代替判断式往往重要但不直观,所以初学者难以理解 OI 常见反演技巧。例如,对于奇质数
有 ;单位根反演 。从判断式到和式的过程形成套路,深入了解其背后的逻辑有助于读者掌握并熟练运用这种套路。
莫比乌斯反演的结论:
- 若
,则 。即若 ,则 。 - 若
,则 。这其实就是上一节末尾提到的 作为容斥系数。验证: 。 - 因为
,所以 ,即 。变式为 。
莫比乌斯反演的常见应用:
别看它只是将
5.4 常见技巧
相当于对 “最大公约数为
考虑单个质因子
5.5 例题
让我们在例题中感受莫比乌斯反演的广泛应用。除特殊说明,以下所有分式均向下取整。
I. P2522 [HAOI2011] Problem b
二维差分将和式下界化为
只有
莫比乌斯反演,得
枚举约数
由于
整除分块即可,时间复杂度
II. SP5971 LCMSUM - LCM Sum
设
当
另一种推导
最后一步是因为
答案为
III. P4318 完全平方数
设
首先去掉
直接计算,时间复杂度
IV. P2257 YY 的 GCD
注意到分母上的
这一步调整了计算顺序使得可通过乘法分配律提出向下取整的式子。
另一种推导方式:考虑对
尽管
V. P3455 [POI2007] ZAP-Queries
P2522 的子问题,代码。
VI. P2568 GCD
P2257 的子问题。
VII. P1829 [国家集训队] Crash 的数字表格
设
根据
莫比乌斯反演,得
注意到后面两个和式不太好化简,我们设
至此已经可以狄利克雷前缀和
时间复杂度
VIII. AT5200 [AGC038C] LCMs
令
设
其中
IX. P3911 最小公倍数之和
双倍经验。
X. P6156 简单题
和上题一样的套路。枚举
令
线性筛预处理出
XI. P6222 「P6156 简单题」加强版
双倍经验,时间复杂度
XII. P3327 [SDOI2015] 约数个数和
利用 5.4 小节的第二个公式,套入莫比乌斯反演,可得答案式
整除分块预处理
XIII. P1447 [NOI2010] 能量采集
XIV. P6810 「MCOI-02」Convex Hull 凸包
XV. P2158 [SDOI2008] 仪仗队
XVI. P3704 [SDOI2017] 数字表格
设
线性或线对预处理
6. 杜教筛
杜教筛可以在亚线性时间内求出满足条件的数论函数前缀和。
6.1 算法介绍
杜教筛公式的推导相当自然,但动机十分巧妙。设希望求出
这说明若
一般可杜教筛函数
另一种理解方式:设第一象限每个整点
复杂度分析:由递推式可知求解
在复杂度分析过程中我们还得到了一个有趣的结论:大于
一般预处理
6.2 例题
I. P4213 【模板】杜教筛(Sum)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
constexpr int N = 5e6 + 5;
bool vis[N];
int cnt, pr[N], mu[N], phi[N], smu[N];
ll sphi[N];
unordered_map<int, ll> p, m;
ll dp(int n) {
if(n < N) return sphi[n];
auto it = p.find(n);
if(it != p.end()) return it->second;
ll sum = 1ll * n * (n + 1ll) / 2;
for(int l = 2, r; ; l = r + 1) {
r = n / (n / l);
sum -= (r - l + 1) * dp(n / l);
if(r == n) break;
}
return p[n] = sum;
}
int dm(int n) {
if(n < N) return smu[n];
auto it = m.find(n);
if(it != m.end()) return it->second;
ll sum = 1;
for(int l = 2, r; l <= n; l = r + 1) {
r = n / (n / l);
sum -= (r - l + 1) * dm(n / l);
if(r == n) break;
}
return m[n] = sum;
}
int main() {
mu[1] = phi[1] = 1;
for(int i = 2; i < N; i++) {
if(!vis[i]) pr[++cnt] = i, mu[i] = -1, phi[i] = i - 1;
for(int j = 1; j <= cnt && i * pr[j] < N; j++) {
vis[i * pr[j]] = 1;
if(i % pr[j] == 0) {
phi[i * pr[j]] = phi[i] * pr[j];
break;
}
phi[i * pr[j]] = phi[i] * (pr[j] - 1);
mu[i * pr[j]] = -mu[i];
}
}
for(int i = 1; i < N; i++) smu[i] = smu[i - 1] + mu[i], sphi[i] = sphi[i - 1] + phi[i];
int T, n;
cin >> T;
while(T--) {
cin >> n;
cout << dp(n) << " " << dm(n) << "\n";
}
return 0;
}
参考文章
第一章:
第三章:
第六章:
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
2021-10-04 分治与根号算法