莫比乌斯反演,欧拉反演学习笔记
(未更完)
我算法中也就差点数论没学了,这几周卷了,学了一下,分享一下啊。
我会讲得详细一点,关于我不懂得地方,让新手更容易理解。
学习反演有很多定义啥的必须要记的,学的时候容易崩溃,所以希望大家能坚持下来。
第一个定义:
数论分块
学习反演之前,要先学习一些边角料,先来看数论分块(又名整除分块)。
最典型的一个例子是求
首先,一个个循环
通过打表可以发现
函数图像长这样:
下面来证明一下:
当
当
那么就可以枚举
具体怎么做呢,我们发现值相同的数成块状分布,假如知道块的第一个是
这个结论模拟一下就能懂,并且很好背。
代码(现场写的):
for (int l = 1, r; l <= n; l = r + 1) { r = n / (n / l); ans += (n / l) * (r - l + 1); }
居然有个板子题,还是绿的:链接,别忘了开
数论分块的变形
形式一:
求
显然:
化为:
在枚举
因为这一段
那么这一段的贡献就是
把
代码(当然也是现场写的):
int ans = n * k;
for (int l = 1, r; l <= n; l = r + 1) { if (k / l) r = min (n, k / (k / l) );
else break;// k / l 已经等于 0 了,乘上 0 不会对答案产生任何的贡献。 ans -= (r - l + 1) * (l + r) / 2 * (k / l); }
形式二:
给定
由于
之前,我们的
for (int l = 1, r; l <= min (n, m); l = r + 1) { r = min (n / (n / l), m / (m / l) ); ans += (r - l + 1) * (n / l) * (m / l); }
形式三:
求
受形式一的启发,为
代码大家可以自己探究一下。
数论分块例题:
第一题:
思路:
首先容斥一下,只要求
先看
粗略证明一下:每个
这样构造的数
那么就成了
再来看怎么求这个,枚举因数
代码:
#include <iostream> const long long mod = 998244353; using namespace std; long long l, r, x, y; long long f (long long n) { long long ret = 0; l = 1; for (; l != n + 1; l = r + 1) { r = n / (n / l); ret = (ret + (r - l + 1) * (n / l) % mod) % mod; } return ret; } int main () { scanf ("%lld%lld", &x, &y); printf ("%d", ( ( (f (y) - f (x - 1) ) % mod + mod) % mod) ); return 0; }
第二题:
拓展题:
想要钻研的同学们可以去做一下。
代码:
#include <iostream> const int mod = 19940417, inv6 = 3323403; using namespace std; long long x, y, l, r; long long f (long long n, long long m) {//求解 sum (i = 1 to n) i * 下取整 (m / i) 的值 long long ret = 0; l = 1; for (; l != n + 1; l = r + 1) { if (m / l) r = min (n, m / (m / l) ); else break; ret = (ret + (l + r) * (r - l + 1) / 2 % mod * (m / l) % mod) % mod; } return ret; } long long sum (long long n) {return n * (n + 1) % mod * (2 * n + 1) % mod * inv6 % mod;} long long fun (long long n, long long m) {//求解 sum (i = 1 to n) i ^ 2 * 下取整 (n / i) * 下取整 (m / i) 的值 long long ret = 0; l = 1; for (; l <= min (n, m); l = r + 1) { r = min (n / (n / l), m / (m / l) ); ret = (ret + (sum (r) - sum (l - 1) ) * (m / l) % mod * (n / l) % mod) % mod; } return ret; } int main () { cin >> x >> y; if (x > y) swap (x, y); cout << ( (x * x % mod - f (x, x) ) * (y * y % mod - f (y, y) ) % mod - (x * x % mod * y % mod - y * f (x, x) % mod - x * f (x, y) % mod + fun (x, y) ) % mod + mod) % mod; return 0; }
一些有用的定义:
数论函数:值域定义在正整数上的函数。
积性函数:对于任何两个正整数
完全积性函数:对于任何两个正整数
艾弗森括号:形如
常见的积性函数:
单位函数
幂函数
常值函数
因数个数函数
因数
欧拉函数
莫比乌斯函数
否则为
如果一个函数是积性函数,那么可以对其线性筛。
这些现在大家可能觉得没什么用,待会儿就知道了。
狄利克雷卷积:
我们定义两个数论函数
性质满足交换律,结合律,分配律。
简单反演:
反演:如果一些数论函数较难求得,但是可以求出它的因数个数,因数和等,可以用反演简化运算。
目前我知道的反演有两种:莫比乌斯反演,欧拉反演。
很多题目两种反演都可以用,下面我们先来介绍一下这两个的主要内容吧。
莫比乌斯反演:
更直白的说:如果
欧拉反演:
例题:
了解了两种反演后,我们来做几道例题。
第一题:
求
首先,暴力是
T1欧拉反演做法:
原式
注意这里如果一个数既是
内层循环和
然后发现内两层循环就是求倍数个数,很容易求得,
所以原式进一步化为:
当然,一般题目不会有这么大的数据范围,线性筛就够用了。
代码(内含线性筛
#include <iostream> #define int long long using namespace std; const int mod = 998244353; int n, m, ans, cnt; bool prime[1000005]; int primes[300005], phi[1000005]; void init () { phi[1] = 1; for (int i = 2; i <= 1000000; i ++){ if (! prime[i]) { primes[++ cnt] = i; phi[i] = i - 1;//i 是质数,1 ~ i - 1 都和它互素 } for (int j = 1; j <= cnt && i * primes[j] <= 1000000; j ++) { prime[i * primes[j] ] = true; if (i % primes[j] == 0) { phi[i * primes[j] ] = phi[i] * primes[j];//i 是 primes[j] 的倍数,此时 phi[i * primes[j] ] = phi[i] * primes[j]。 break; } phi[i * primes[j] ] = phi[i] * (primes[j] - 1);//i 和 primes[j] 互素,根据积性函数定义。 //phi[i * primes[j] ] = phi[i] * phi[primes[j] ],即 phi[i] * (primes[j] - 1)。 } phi[i] = (phi[i] + phi[i - 1]) % mod;//预处理前缀和 + 取模。 } } signed main () { init (); cin >> n >> m; if (n > m) swap (n, m); for (int l = 1, r; l <= n; l = r + 1) {//整除分块。 r = min (n / (n / l), m / (m / l) ); ans = (ans + (phi[r] - phi[l - 1]) * (n / l) % mod * (m / l) % mod) % mod; } cout << ans; return 0; }
T1莫比乌斯反演做法:
若
化为
此时使用莫反:
枚举
内两层循环也是求倍数个数,化简为
原式化为:
线性筛
(后面我将不再详细介绍每一步反演的过程,仅仅选择重要的几步介绍。)
线性筛
#include <iostream> using namespace std; int cnt; int primes[300005], mu[1000005]; bool primes[1000005]; int main () { for (int i = 2; i <= 1000000; i ++) { if (!prime[i]) { primes[++ cnt] = i; mu[i] = -1;//仅有一个质因子,它本身。注意:1不是质数。 } for (int j = 1; j <= cnt && i * primes[j] <= 1000000; j ++) { prime[i * primes[j] ] = true; if (i % primes[j] == 0) { mu[i * primes[j] ] = 0;//含有平方因子 primes[j] * primes[j]。 break; } mu[i * primes[j] ] = -mu[i];//多了一个因子 primes[j],乘上 -1,或者根据积性函数的定义。 } } return 0; }
第二题:
这题貌似只能用莫比乌斯反演,大家可以先自己尝试一下,这个比较简单。
T2莫比乌斯反演做法:
预处理
(可能大家觉得整除分块可以不用,因为时间复杂度瓶颈在线性筛。但是学了杜教筛之后,这题的数据范围就可以到
第三题:
这题也不能用欧拉反演做,因为
T3莫比乌斯反演做法:
枚举
内层
然后发现是个狄利克雷卷积。
预处理
第四题:
设
我会尽量写的详细些。
枚举
大家可能还没发现后面可以用整除分块,我们令
这下一目了然,
注意这里
我们再来讲一下这个东西怎么整除分块:
这时有两个选择,两层整除分块:
预处理
怎么预处理呢?我们发现
预处理
代码(内含线性筛
UPD on 2023/04/05:今天突然发现算内层整除分块时顺便记忆化也可以,还更方便,虽然是
#include <iostream> using namespace std; int T, n, m; int cnt; long long mu[50005], num[50005], sigma[50005], primes[20005];//num[i] 表示 i 最小质因子的数量,看到后面注释就能明白 bool prime[50005]; void init () { mu[1] = sigma[1] = 1; for (int i = 2; i <= 50000; i ++) { if (!prime[i]) { primes[++ cnt] = i; sigma[i] = 2;//素数有两个约数,它本身和 1 mu[i] = -1; num[i] = 1; } for (int j = 1; j <= cnt && i * primes[j] <= 50000; j ++) { prime[i * primes[j] ] = true; if (i % primes[j] == 0) { mu[i * primes[j] ] = 0;//有平方因子 sigma[i * primes[j] ] = sigma[i] / (num[i] + 1) * (num[i] + 2); /*设 i 被分解后各项的次方分别是:a_1,a_2,a_3...a_n。(底数从小到大排列的) 在整除分块例题中证明过 d[i] = (a_1 + 1) * (a_2 + 1) * ... * (a_n + 1) 现在多了一个最小质因数,d[i * primes[j] ] = (a_1 + 2) * (a_2 + 1) * ... * (a_n + 1) 所以我们需要记录 a_1,也就是最小质因子的数量。 */ num[i * primes[j] ] = num[i] + 1;//那么最小质因子的数量显然加上一 break; } mu[i * primes[j] ] = -mu[i]; sigma[i * primes[j] ] = sigma[i] * 2;//i 所有的因子乘上 primes[j] 可以构造出新的因子。 num[i * primes[j] ] = 1;//primes[j] 是 i * primes[j] 的最小质因数。 //而 i % primes[j] != 0,所以 num[i * primes[j] ] = 1 } sigma[i] += sigma[i - 1];//预处理前缀和 mu[i] += mu[i - 1]; } } int f (int x) {return sigma[x];}//求解 Σ下取整 (x/i),根据刚刚的推论,它就等于 sigma[1] +... + sigma[x] int main () { init (); scanf ("%d", &T); while (T --) { long long ans = 0; scanf ("%d%d", &n, &m); if (n > m) swap (n, m); for (int l = 1, r; l <= n; l = r + 1) {//整除分块 r = min (n / (n / l), m / (m / l) ); ans += long (mu[r] - mu[l - 1]) * f (n / l) * f (m / l); //这一段 n' m' 的值是一样的,可以把 mu 提出来用前缀和。 } printf ("%lld\n", ans); } return 0; }
记忆化代码:
#include <iostream> using namespace std; int T, n, m, cnt; int mem[50005]; long long primes[50005], mu[50005]; bool prime[50005]; void init () { mu[1] = 1; for (int i = 2; i <= 50000; i ++) { if (!prime[i]) { primes[++ cnt] = i; mu[i] = -1; } for (int j = 1; j <= cnt && i * primes[j] <= 50000; j ++) { prime[i * primes[j] ] = true; if (i % primes[j] == 0) break; mu[i * primes[j] ] = -mu[i]; } mu[i] += mu[i - 1]; } } long long f (int x) { if (mem[x]) return mem[x]; for (int l = 1, r; l <= x; l = r + 1) { r = x / (x / l); mem[x] += (r - l + 1) * (x / l); } return long (mem[x]); } int main () { init (); scanf ("%d", &T); long long ans; while (T --) { ans = 0; scanf ("%d%d", &n, &m); if (n > m) swap (n, m); for (int l = 1, r; l <= n; l = r + 1) { r = min (n / (n / l), m / (m / l) ); ans += long (mu[r] - mu[l - 1]) * f (n / l) * f(m / l); } printf ("%lld\n", ans); } return 0; }
总结:
反演时通常考虑枚举一个数,然后快速求出因数个数啥的;
如果不能继续反演,可以考虑枚举两个数相乘啥的,就能从绝境中走出。
反演时两种都试一下,选择最好写,时间复杂度合适的算法。
杜教筛:
杜教筛也是反演的基础,我们先来了解一下它吧。
介绍:
杜教筛三问:名字怎么来的?有什么用?时间复杂度如何?
(大家可能觉得
例题:
给定一个正整数,求:
我们一边讲题一边介绍杜教筛吧。
考虑构造数论函数使
枚举
内层
所以
然后把
设
又化简为
所以
后面可以整除分块,前面
加上记忆化,时间复杂度为
求 前 项和:
取
带回得到
进一步化简得到:
代码:
int sum_phi (int n) {//求 s(n) 的值 if (n <= b) return s[n];//提前筛好的 if (map[n]) return map[n];//记忆化 int ans = n * (n + 1) / 2; for (int l = 2, r; l <= n; l = r + 1) {//整除分块 r = n / (n / l); ans -= sum_phi (n / l) * (r - l + 1); } return map[n] = ans;//记忆化 }
求 前 项和:
依然取
化简为
int sum_mu (int n) { if (n <= b) return s[n]; if (map[n]) return map[n]; int ans = 1; for (int l = 2, r; l <= n; l = r + 1) { r = n / (n / l); ans -= sum_phi (n / l) * (r - l + 1) / 2; } return map[n] = ans;//记忆化 }
例题:
恭喜你已经学完了大部分反演的内容,我们做几道例题。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异