『学习笔记』浅谈莫比乌斯反演
莫比乌斯反演是数论中的重要内容。对于一些函数 \(f(n)\),如果很难直接求出它的值,而容易求出其倍数和或约数和 \(g(n)\),那么可以通过莫比乌斯反演简化运算,求得 \(f(n)\) 的值。——OI-wiki
可见莫反的强大。
前置知识:数论分块
数论分块可以快速计算一些含有除法向下取整的和式,如 \(\sum\limits_{i=1}^n f(i)\times g\left(\left\lfloor\dfrac{n}{i}\right\rfloor\right)\)。由于 \(y=\left\lfloor\dfrac nx\right\rfloor\) 的图像是成阶梯式的变化,便对于一段满足任意两个 \(i,j\) 都满足 \(\left\lfloor\dfrac ni\right\rfloor=\left\lfloor\dfrac nj\right\rfloor\)的区间 \([l,r]\),其对应的答案为 \(g\left(\left\lfloor\dfrac ni\right\rfloor\right)\times\sum\limits_{i=l}^rf(i)\),后面的便可以提前预处理出。
但是这个 \([l,r]\) 怎么求呢?
我们令 \(k=\left\lfloor\dfrac nl\right\rfloor\),则有 \(k\le\dfrac nl\)。从而 \(\left\lfloor\dfrac nk\right\rfloor\ge\left\lfloor\dfrac n{\frac nl}\right\rfloor=l\),得 \(r\le\left\lfloor\frac n{\left\lfloor\frac nl\right\rfloor}\right\rfloor\),即 \(r_{\max}=\left\lfloor\frac n{\left\lfloor\frac nl\right\rfloor}\right\rfloor\)。
例题:UVa11526 H(n)
题意:求
板子题,代码如下:
#include<bits/stdc++.h>
#define int long long
using namespace std;
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int n, ans = 0, l = 1;
cin >> n;
while(l <= n) {
int r = n / (n / l);
ans += (r - l + 1) * (n / l);
l = r + 1;
}
cout << ans;
return 0;
}
数论分块一般配合莫反进一步降低时间复杂度。
习题
定义
\(\mu\) 为莫比乌斯函数,定义为
令 \(n=\prod\limits^k_{i=1}p_i^{c_i}\)。
- 若 \(n=1\),\(\mu(n)=1\);
- 否则:
- 若 \([c_i>1]\neq0\),则存在 $p_i^2\mid n $,此时 \(\mu(n)=0\)。
- 否则每个质因子都只出现过一次,此时 \(\mu(n)=(-1)^k\)。
性质
首先,莫比乌斯函数为积性函数,并且满足
接着给一下反演的结论:\([\gcd(i,j)=1]=\sum\limits_{d\mid\gcd(i,j)}\mu(d)\)。
求法
因为莫比乌斯函数为积性函数,所以可用线性筛来求解。
代码如下:
inline void getMu() {
mu[1] = 1;
for(int i = 2; i <= n; ++i) {
if(!vis[i]) {
p[++tot] = i;
mu[i] = -1;
}
for(int j = 1; j <= tot && i * p[j] <= n; ++j) {
vis[i * p[j]] = 1;
if(i % p[j] == 0) {
mu[i * p[j]] = 0;
break;
}
mu[i * p[j]] = -mu[i];
}
}
}
变换
令 \(f(x),g(x)\) 为两个数论函数。
形式 \(1\):若 \(f(n)=\sum\limits_{d\mid n}g(d)\),则 \(g(n)=\sum\limits_{d\mid n}\mu(d)f\left(\frac nd\right)\)。
形式 \(2\):若 \(f(n)=\sum\limits_{n\mid d}g(d)\),则 \(g(n)=\sum\limits_{n|d}\mu\left(\frac dn\right)f(d)\)。
这时,我们称 \(f(x)\) 为 \(g(x)\) 的莫比乌斯变换,\(g(x)\) 为 \(f(x)\) 的莫比乌斯逆变换(反演)。
例题
eg1. P2522 [HAOI2011] Problem b
很经典的一题,求:
考虑前缀和,对于 \((a,b,c,d)=(1,1,n,m)\),进行变换。
若 \(\gcd(i,j)=k\),则 \(\gcd(i\div k,j\div k)=1\),所以化为:
而 \([\gcd(i,j)=1]\) 可通过反演结论化为 \(\sum\limits_{d\mid\gcd(i,j)}\mu(d)\):
变换求和顺序,得:
继续变化得:
显然可以用数论分块求,总时间复杂度为 \(\mathcal{O}(n+t\sqrt{n})\)。
代码如下:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e4 + 5;
int n, k, p[N], tot, f[N], sum[N];
bool vis[N];
inline int s(int a, int b) {
a /= k, b /= k;
int l = 1, ans = 0;
while(l <= min(a, b)) {
int r = min(a / (a / l), b / (b / l));
ans += (sum[r] - sum[l - 1]) * (a / l) * (b / l);
l = r + 1;
}
return ans;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
f[1] = vis[1] = 1;
for(int i = 2; i < N; i++) {
if(!vis[i]) {
p[++tot] = i;
f[i] = -1;
}
vis[i] = 1;
for(int j = 1; j <= tot && i * p[j] < N; j++) {
vis[i * p[j]] = 1;
if(i % p[j] == 0) {
f[i * p[j]] = 0;
break;
}
f[i * p[j]] = -f[i];
}
}
for(int i = 1; i < N; i++) {
sum[i] = sum[i - 1] + f[i];
}
cin >> n;
while(n--) {
int a, b, c, d;
cin >> a >> b >> c >> d >> k;
cout << s(b, d) - s(a - 1, d) - s(b, c - 1) + s(a - 1, c - 1) << '\n';
}
return 0;
}
*注:本体还有一个弱化版,link。
eg2. P1891 疯狂 LCM
求:
不难想到转换为
首尾配对得:
通过辗转相减法得:
再合并得:
而 \(\gcd(i,n)=d\) 的个数等于 \(\gcd\left(\dfrac id,\dfrac nd\right)=1\) 的个数等于 \(\varphi\left(\dfrac nd\right)\),于是进一步转换为:
所以 \(g(n)\) 为积性函数,考虑用线性筛求出。
由于本人实力有限,便只给出结论:
线性筛部分代码:
for(int i = 2; i < N; i++) {
if(!vis[i]) {
p[++tot] = i;
f[i] = i * (i - 1) + 1;
}
for(int j = 1; j <= tot && i * p[j] < N; j++) {
vis[i * p[j]] = 1;
if(i % p[j] == 0) {
f[i * p[j]] = f[i] + (f[i] - f[i / p[j]]) * p[j] * p[j];
break;
}
f[i * p[j]] = f[i] * f[p[j]];
}
}
总时间复杂度为 \(\mathcal{O}(n+t)\)。
代码如下:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6 + 5;
int n, p[N], tot, f[N];
bool vis[N];
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
f[1] = 1;
for(int i = 2; i < N; i++) {
if(!vis[i]) {
p[++tot] = i;
f[i] = i * (i - 1) + 1;
}
for(int j = 1; j <= tot && i * p[j] < N; j++) {
vis[i * p[j]] = 1;
if(i % p[j] == 0) {
f[i * p[j]] = f[i] + (f[i] - f[i / p[j]]) * p[j] * p[j];
break;
}
f[i * p[j]] = f[i] * f[p[j]];
}
}
int t;
cin >> t;
while(t--) {
int n;
cin >> n;
cout << (f[n] + 1) * n / 2 << '\n';
}
return 0;
}
eg3. SP26017 GCDMAT - GCD OF MATRIX
求:
同样考虑前缀和,即求 \(\sum\limits_{i=1}^n\sum\limits_{j=1}^m\gcd(i,j)\)。
变换(令 \(n\le m\)):
反演结论:
枚举 \(p\):
再令 \(q=pd\),化为:
而 \(\sum\limits_{p\mid q}\dfrac qp\cdot\mu(p)=\sum\limits_{i=1}^q\varphi(i)\) 可用线性筛求出,前面可用数论分块求解,总时间复杂度为 \(\mathcal{O}(n+T\sqrt n)\)。
代码如下:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e7 + 5, Mod = 1e9 + 7;
int n, m, p[N], tot, f[N], sum[N];
bool vis[N];
inline int s(int x, int y) {
int l = 1, ans = 0;
if(x > y) {
swap(x, y);
}
while(l <= x) {
int r = min(x / (x / l), y / (y / l));
ans = (ans + (sum[r] - sum[l - 1]) * (x / l) % Mod * (y / l) % Mod) % Mod;
l = r + 1;
}
return ans;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int t;
cin >> t >> n >> m;
f[1] = 1;
for(int i = 2; i <= min(n, m); i++) {
if(!vis[i]) {
p[++tot] = i;
f[i] = i - 1;
}
for(int j = 1; j <= tot && i * p[j] <= min(n, m); j++) {
vis[i * p[j]] = 1;
if(i % p[j] == 0) {
f[i * p[j]] = f[i] * p[j] % Mod;
break;
}
f[i * p[j]] = f[i] * (p[j] - 1) % Mod;
}
}
for(int i = 1; i <= min(n, m); i++) {
sum[i] = (sum[i - 1] + f[i]) % Mod;
}
while(t--) {
int a, b, c, d;
cin >> a >> b >> c >> d;
cout << ((s(c, d) - s(a - 1, d) - s(c, b - 1) + s(a - 1, b - 1)) % Mod + Mod) % Mod << '\n';
}
return 0;
}
此题还有一个加强版,但本人不会卡常(doge,感兴趣的可以做一做,SP26045 GCDMAT2 - GCD OF MATRIX (hard)。
eg4. P1829 [国家集训队] Crash的数字表格 / JZPTAB
求:
显然能化为:
套路:
改变求和顺序:
通过反演结论得(令 \(n'=\left\lfloor\dfrac nd\right\rfloor\le \left\lfloor\dfrac md\right\rfloor=m'\)):
套路:
令后一段为 \(\operatorname{s}(n,m)\),从而原式等于
而 \(\operatorname{s}(n,m)\) 内部的前半段可以线性筛预处理,后半段可用数论分块求解。
而整体的答案又可以再用一遍数论分块,注意其中细节比较多(尤其是取模)。
总时间复杂度为 \(\mathcal{O}(n+\sqrt{m}\cdot\sqrt{m})=\mathcal{O}(n+m)\)。
代码如下:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e7 + 5, Mod = 20101009;
int n, m, p[N], tot, f[N], sum[N];
bool vis[N];
inline int s(int x, int y) {
int l = 1, ans = 0;
while(l <= min(x, y)) {
int r = min(x / (x / l), y / (y / l));
int tmp = ((x / l) * (x / l + 1) / 2 % Mod) * ((y / l) * (y / l + 1) / 2 % Mod) % Mod;
ans = (ans + (sum[r] - sum[l - 1] + Mod) % Mod * tmp % Mod) % Mod;
l = r + 1;
}
return ans;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
f[1] = 1;
for(int i = 2; i < N; i++) {
if(!vis[i]) {
p[++tot] = i;
f[i] = -1;
}
for(int j = 1; j <= tot && i * p[j] < N; j++) {
vis[i * p[j]] = 1;
if(i % p[j] == 0) {
f[i * p[j]] = 0;
break;
}
f[i * p[j]] = -f[i];
}
}
for(int i = 1; i < N; i++) {
sum[i] = (sum[i - 1] + i * i % Mod * (f[i] + Mod) % Mod) % Mod;
}
int l = 1, ans = 0;
while(l <= min(n, m)) {
int r = min(n / (n / l), m / (m / l));
ans = (ans + (r - l + 1) * (l + r) / 2 % Mod * s(n / l, m / l) % Mod) % Mod;
l = r + 1;
}
cout << ans;
return 0;
}
求:
可化为:
下面化简 \(d(i,j)\)。
再带回到原式得(令 \(n\le m\),\(S(n)=\sum\limits_{i=1}^nd(i)\)):
而 \(S(n)\) 和 \(\mu(n)\) 可线性筛预处理,再加上数论分块,总时间复杂度为 \(\mathcal{O}(n+t\sqrt{n})\)。
代码如下:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 50005;
int T, p[N], tot, f[N], d[N], t[N];
bool vis[N];
inline int s(int n, int m) {
int l = 1, ans = 0;
if(n > m) {
swap(n, m);
}
while(l <= n) {
int r = min(n / (n / l), m / (m / l));
ans += d[n / l] * d[m / l] * (f[r] - f[l - 1]);
l = r + 1;
}
return ans;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
f[1] = d[1] = 1;
for(int i = 2; i < N; i++) {
if(!vis[i]) {
p[++tot] = i;
f[i] = -1;
d[i] = 2;
t[i] = 1;
}
for(int j = 1; j <= tot && i * p[j] < N; j++) {
vis[i * p[j]] = 1;
if(i % p[j] == 0) {
f[i * p[j]] = 0;
d[i * p[j]] = d[i] / (t[i] + 1) * (t[i] + 2);
t[i * p[j]] = t[i] + 1;
break;
}
f[i * p[j]] = -f[i];
d[i * p[j]] = d[i] << 1;
t[i * p[j]] = 1;
}
}
for(int i = 2; i < N; i++) {
f[i] += f[i - 1];
d[i] += d[i - 1];
}
cin >> T;
while(T--) {
int n, m;
cin >> n >> m;
cout << s(n, m) << '\n';
}
return 0;
}

浙公网安备 33010602011771号