莫比乌斯反演套路
简介&&吐槽
莫比乌斯反演,通常又可以称为“懵逼钨丝繁衍”
这样一种听上去就很高端的知识是做什么的?
莫比乌斯反演是数论数学中很重要的内容,可以用于解决很多组合数学的问题。
这和没说有什么区别……事实上,莫比乌斯反演充满了套路。
我们开心的来看一下它的定义吧。
不过在这之前,我们先补一些有趣的小知识。
1.数论函数的定义
在数论上,算术函数(或称数论函数)指定义域为正整数、陪域为复数的函数,每个算术函数都可视为复数的序列。
就我们oi选手而言,简单的理解为定义域为正整数的函数即可。
2.积性函数与完全积性函数
积性函数:对于所有
gcd(a,b)=1
g
c
d
(
a
,
b
)
=
1
,满足
f(ab)=f(a)f(b)
f
(
a
b
)
=
f
(
a
)
f
(
b
)
完全记性函数:对于所有
a,b
a
,
b
,满足
f(ab)=f(a)f(b)
f
(
a
b
)
=
f
(
a
)
f
(
b
)
3.常见的积性函数
3.1 欧拉函数 φ φ
φ(n)
φ
(
n
)
表示
[1,n]
[
1
,
n
]
中与
n
n
互质的数的个数
,其中,
P
P
是的不同因子集合
3.2 莫比乌斯函数 μ μ
若
n
n
有平方因子,则
否则,
n
n
为个质因数的乘积,则
μ(n)=(−1)k
μ
(
n
)
=
(
−
1
)
k
3.3 幂函数 Id I d
Idk(n)
I
d
k
(
n
)
表示
nk
n
k
特别的,有
Id0(n)=1(n)=1
I
d
0
(
n
)
=
1
(
n
)
=
1
Id1(n)=Id(n)=n I d 1 ( n ) = I d ( n ) = n
3.4 单位函数 ϵ ϵ
ϵ(n)={1(n=1)0(n>1)
ϵ
(
n
)
=
{
0
(
n
>
1
)
1
(
n
=
1
)
这个函数如果用群论的思想来理解的话就相当于一个幺元。
4. Dirichlet Convolution D i r i c h l e t C o n v o l u t i o n
——狄利克雷卷积
定义两数论函数
f,g
f
,
g
的
Dirichlet
D
i
r
i
c
h
l
e
t
卷积
注意:卷积 (f∗g)(n) ( f ∗ g ) ( n ) 中的 ∗ ∗ 不是乘号,而是卷积的表示
4.1 卷积的性质
交换律:
f∗g=g∗f
f
∗
g
=
g
∗
f
结合律:
f∗(g∗h)=(f∗g)∗h
f
∗
(
g
∗
h
)
=
(
f
∗
g
)
∗
h
分配率:
f∗(g+h)=f∗g+f∗h
f
∗
(
g
+
h
)
=
f
∗
g
+
f
∗
h
单位元:
f∗ϵ=f
f
∗
ϵ
=
f
若
f,g
f
,
g
均为积性函数,则
(f∗g)
(
f
∗
g
)
仍为积性函数
具体的证明略去(太难写了)
4.2 常见的 Dirichlet D i r i c h l e t 卷积
我们在这里只列举常用的两个卷积式子
φ(n)=∑d|nμ(d)nd
φ
(
n
)
=
∑
d
|
n
μ
(
d
)
n
d
,即
φ(n)=(μ∗Id)(n)
φ
(
n
)
=
(
μ
∗
I
d
)
(
n
)
ϵ(n)=∑d|nμ(d)
ϵ
(
n
)
=
∑
d
|
n
μ
(
d
)
,即
ϵ(n)=(μ∗1)(n)
ϵ
(
n
)
=
(
μ
∗
1
)
(
n
)
我们可以证明一下第二个式子(这个式子在后面很常用)
假设
n
n
有个不同的质因子,则有
我们来理解一下这个式子。
首先,如果 n n 的因数中含有平方因子,则显然有 μ(d)=0 μ ( d ) = 0 ,可以从和式中略去
那么对于一个数 d=p1∗p2∗…∗pm d = p 1 ∗ p 2 ∗ … ∗ p m (其中, p1,p2,…pm p 1 , p 2 , … p m 为 d d 的质因子),根据函数的定义,显然有 μ(d)=(−1)m μ ( d ) = ( − 1 ) m
那么从 n n 的个质因子中任意选出 m m 个之后,这些质因子的乘积设为,一定有 μ(x) μ ( x ) 的值均为 (−1)m ( − 1 ) m ,并且这样的方案恰有 (km) ( m k ) 种。
它们的总和就得到了 ∑ki=0(−1)i(ki) ∑ i = 0 k ( − 1 ) i ( i k )
显然有 ∑ki=0(−1)i(ki)=∑ki=0(−1)i(1)k−i(ki) ∑ i = 0 k ( − 1 ) i ( i k ) = ∑ i = 0 k ( − 1 ) i ( 1 ) k − i ( i k )
根据二项式定理,我们最后得到
这个式子当且仅当 n=1 n = 1 时等于 1 1 ,原式得证。
5.莫比乌斯反演
5.1 莫比乌斯反演的形式
如果有两个函数,满足
则它们也满足
反之亦然,即
5.2 莫比乌斯反演的证明
网上常见的证明方式都是用和式的变换来做的。我们今天就用
Dirichlet
D
i
r
i
c
h
l
e
t
卷积来证明
设
我们将等式两侧同时卷上 μ μ ,得
(这一步我们刚才在常见的 Dirichlet D i r i c h l e t 卷积中证明过)
整理得
反过来同理
这样看来莫比乌斯反演的正确性就非常显然了,只要我们确信 μ∗1=ϵ μ ∗ 1 = ϵ 即可
5.3 莫比乌斯反演的应用方式
事实上,我们的莫比乌斯反演通常是用来改变枚举的顺序,从而达到简化计算的目的。
举一个抽象的例子,我们已知
f=(g∗1)(n)
f
=
(
g
∗
1
)
(
n
)
,如果说
f
f
是一个易于求解的函数,那么我们就可以通过反演的形式来求得g的函数值。
6.莫比乌斯函数的常见套路
6.1改变和式的枚举顺序
已知 F(n)=∑i=1n∑d|if(d)g(id) F ( n ) = ∑ i = 1 n ∑ d | i f ( d ) g ( i d )
⇒ F(n)=∑d=1n∑i=1⌊nd⌋f(d)g(i)⇒ F(n)=∑d=1nf(d)∑i=1⌊nd⌋g(i) ⇒ F ( n ) = ∑ d = 1 n ∑ i = 1 ⌊ n d ⌋ f ( d ) g ( i ) ⇒ F ( n ) = ∑ d = 1 n f ( d ) ∑ i = 1 ⌊ n d ⌋ g ( i )
令 S(n)=∑ni=1g(i) S ( n ) = ∑ i = 1 n g ( i ) ,则原式得
6.2 由 ϵ ϵ 到莫比乌斯反演
首先,我们常常会看到这样的一种表达
[gcd(i,j)==1]
[
g
c
d
(
i
,
j
)
==
1
]
。
事实上,这个记号
[]
[
]
可以等价于
ϵ()
ϵ
(
)
例如:
已知
∑i=1n∑j=1m[gcd(i,j)==1]
∑
i
=
1
n
∑
j
=
1
m
[
g
c
d
(
i
,
j
)
==
1
]
由于
ϵ=μ∗1
ϵ
=
μ
∗
1
⇒∑i=1n∑j=1m∑d|gcd(i,j)μ(d)
⇒
∑
i
=
1
n
∑
j
=
1
m
∑
d
|
g
c
d
(
i
,
j
)
μ
(
d
)
此时,根据套路1,改变枚举顺序,首先枚举
d
d
这个式子就算是暴力枚举也可以在 O(n) O ( n ) 的时间内完成,如果了解数论分块的小伙伴还可以在 O(n−−√) O ( n ) 的时间内求出答案。
6.3将复合的条件提出
接下来,我们的套路就会给出一些例题了
bzoj1101 [POI2007]Zap
Description
FGD正在破解一段密码,他需要回答很多类似的问题:对于给定的整数a,b和d,有多少正整数对x,y,满足x<=a,y<=b,并且gcd(x,y)=d。作为FGD的同学,FGD希望得到你的帮助。
Input
第一行包含一个正整数n,表示一共有n组询问。(1<=n<= 50000)接下来n行,每行表示一个询问,每行三个正整数,分别为a,b,d。(1<=d<=a,b<=50000)
Output
对于每组询问,输出到输出文件zap.out一个正整数,表示满足条件的整数对数。
Sample Input
2
4 5 2
6 4 3Sample Output
3
2Hint
对于第一组询问,满足条件的整数对有(2,2),(2,4),(4,2)。对于第二组询问,满足条件的整数对有(6,3),(3,3)。
简明题意:给定
n,m,d
n
,
m
,
d
,求
∑ni=1∑mj=1[gcd(i,j)==d]
∑
i
=
1
n
∑
j
=
1
m
[
g
c
d
(
i
,
j
)
==
d
]
我们发现这个东西和套路2特别像,只不过区别是
[gcd(i,j)==d]
[
g
c
d
(
i
,
j
)
==
d
]
还是
[gcd(i,j)==1]
[
g
c
d
(
i
,
j
)
==
1
]
由于我们的
ϵ
ϵ
函数只能够表达
[gcd(i,j)==1]
[
g
c
d
(
i
,
j
)
==
1
]
的情况,所以我们这里就要把原式转换为这种形式。
首先,我们先给出一个简单的式子:
令
n′=nd,m′=md
n
′
=
n
d
,
m
′
=
m
d
,那么
这个东西特别显然,如果不能够很快接受的话可以想一下辗转相除法。
那么我们就可以把这个问题转换成为套路2了
套用套路2
所以我们就可以在 O(n−−√) O ( n ) 内解决问题
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int Maxn=50005;
inline int read() {
char c; int rec=0;
while((c=getchar())<'0'||c>'9');
while(c>='0'&&c<='9') rec=rec*10+c-'0',c=getchar();
return rec;
}
int np[Maxn],prime[Maxn],miu[Maxn],sum[Maxn],psize;
void Getlist() {
sum[1]=miu[1]=1;
for(int i=2;i<=50000;++i) {
if(np[i]==0) {
prime[++psize]=i; miu[i]=-1;
}
for(int j=1;j<=psize&&i*prime[j]<=50000;++j) {
np[i*prime[j]]=1;
if(i%prime[j]==0){miu[i*prime[j]]=0; break;}
miu[i*prime[j]]=-miu[i];
}
sum[i]=sum[i-1]+miu[i];
} return ;
}
int main() {
int ans;
int n=read(); Getlist();
for(int i=1;i<=n;++i) {
int a=read(),b=read(),d=read(),last,j;
a/=d; b/=d; if(a>b) swap(a,b); ans=0;
for(j=1;j*j<=a;++j) ans+=(a/j)*(b/j)*miu[j];
for(;j<=a;j=last+1) {
last=min(a/(a/j),b/(b/j));
ans+=(sum[last]-sum[j-1])*(a/j)*(b/j);
}
cout<<ans<<'\n';
}
return 0;
};
int main() {
return 0;
}
例题时间到
【HAOI2011】Problem b
显然,差分一下就可以了
【BZOJ 2818】Gcd
我们直接枚举所有的质数,然后再用套路3,时间复杂度。。。。总之小于 O(nn−−√) O ( n n )
6.4 枚举两数之积
我们首先来看一个常见的经典题目
[bzoj2820] YY的GCD
Description
神犇YY虐完数论后给傻×kAc出了一题
给定N, M,求1<=x<=N, 1<=y<=M且gcd(x, y)为质数的(x, y)有多少对
kAc这种傻×必然不会了,于是向你来请教……
多组输入Input
第一行一个整数T 表述数据组数
接下来T行,每行两个正整数,表示N, MOutput
T行,每行一个整数表示第i组数据的结果
Sample Input
2
10 10
100 100Sample Output
30
2791Hint
T = 10000
N, M <= 10000000
这个题和之前的套路3有点像,很容易想到直接枚举所有的质数然后套用套路2来解决问题
但是问题来了:多组询问……
我们能不能够做到更快的解决问题呢?
我们来大力推一下式子(套路)
求
套路3,枚举一下质数p
套路2,将最后的 ϵ ϵ 式子改成有关 μ μ 的卷积
这就是我们在做上一道题【BZOJ 2818】Gcd 最后化出来的式子
但是这样显然还不够优秀啊,一次询问就是。。。大概接近 O(n) O ( n ) 的时间复杂度, T T 次询问之后就会超时然后gg
所以我们看到了这个神奇的变量(雾)
设 T=dp T = d p ,那么式子可以暂时先化一步
套路1,首先枚举T
23333,套路嘛,肯定是要一个套一个的。我们的套路4就很好的证明了这一点,套路123叠加就成为了套路4。
现在这个式子,我们只要能够预处理出 ∑k|T,k∈Primeμ(Tk) ∑ k | T , k ∈ P r i m e μ ( T k ) 这一段就能够在 O(n−−√) O ( n ) 的时间内解决每一次询问了。
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int Maxn=10000005;
inline int read() {
char c; int rec=0;
while((c=getchar())<'0'||c>'9');
while(c>='0'&&c<='9') rec=rec*10+c-'0',c=getchar();
return rec;
}
int T,n,m;
int np[Maxn],prime[Maxn],miu[Maxn],sum[Maxn],g[Maxn],psize;
void Getlist() {
miu[1]=1;
for(int i=2;i<=10000000;++i) {
if(np[i]==0) {
prime[++psize]=i; miu[i]=-1;
}
for(int j=1;j<=psize&&i*prime[j]<=10000000;++j) {
np[i*prime[j]]=1;
if(i%prime[j]==0){miu[i*prime[j]]=0; break;}
miu[i*prime[j]]=-miu[i];
}
}
for(int i=1;i<=psize;++i)
for(int j=1;j*prime[i]<=10000000;++j)
g[j*prime[i]]+=miu[j];
for(int i=1;i<=10000000;++i) sum[i]=g[i]+sum[i-1];
//预处理出后面那块式子的前缀和
return ;
}
int main() {
long long ans;
T=read(); Getlist();
while(T--) {
n=read(); m=read();
if(n>m) swap(n,m);
int last,i; ans=0;
for(i=1;i*i<=n;++i) ans+=(long long)(n/i)*(m/i)*g[i];
for(;i<=n;i=last+1) {
last=min(n/(n/i),m/(m/i));
ans+=(long long)(sum[last]-sum[i-1])*(n/i)*(m/i);
}
cout<<ans<<'\n';
}
return 0;
}
那么怎样才能够掌握求出后面这一块式子的套路呢?
当然是吃瓜群众我们喜闻乐见的套路5啦。
6.5 暴力计算一类函数的前缀和
我们假设有这样一个函数 h(n)=(f∗g)(n) h ( n ) = ( f ∗ g ) ( n ) ,换句话说就是 h(n)=∑d|nf(d)g(nd) h ( n ) = ∑ d | n f ( d ) g ( n d )
那么计算 T(n)=∑ni=1h(i) T ( n ) = ∑ i = 1 n h ( i ) 的暴力方案来了:
fori=1ton f o r i = 1 t o n
forj=1to⌊ni⌋ f o r j = 1 t o ⌊ n i ⌋
h(ij)=h(ij)+f(i)g(j) h ( i j ) = h ( i j ) + f ( i ) g ( j )
fori=1ton f o r i = 1 t o n
T(i)=T(i−1)+h(i) T ( i ) = T ( i − 1 ) + h ( i )
这个暴力算法的时间复杂度是多少呢?
为 O(∑ni=1ni)=O(n∑ni=11i)=O(n ln n) O ( ∑ i = 1 n n i ) = O ( n ∑ i = 1 n 1 i ) = O ( n l n n )
回到之前的题目,我们要处理的式子就是
这个例子其实并不标准,因为只有质数我们才会加入处理。
所以我们的第一个循环并不需要枚举每一个可能的因数 i i ,只用枚举范围内的质数即可。
由于 [1,n] [ 1 , n ] 中的质数有 nln n n l n n 个,最后这个式子可以在 O(nln n∗ln n)=O(n) O ( n l n n ∗ l n n ) = O ( n ) 的时间内预处理完毕。
6.6 套路小结
这里就先写这些套路。事实上还有其他的的一些套路(比如有关 φ φ 函数的套路, lcm l c m 的套路),但是基本上都可以从这5个基本套路中提炼出来。
当然,除了套路,对题目本身的敏感和对数学原理的了解程度都能够助你完成题目。
就到这里了。