莫比乌斯反演套路
简介&&吐槽
莫比乌斯反演,通常又可以称为“懵逼钨丝繁衍”
这样一种听上去就很高端的知识是做什么的?
莫比乌斯反演是数论数学中很重要的内容,可以用于解决很多组合数学的问题。
这和没说有什么区别……事实上,莫比乌斯反演充满了套路。
我们开心的来看一下它的定义吧。
不过在这之前,我们先补一些有趣的小知识。
1.数论函数的定义
在数论上,算术函数(或称数论函数)指定义域为正整数、陪域为复数的函数,每个算术函数都可视为复数的序列。
就我们oi选手而言,简单的理解为定义域为正整数的函数即可。
2.积性函数与完全积性函数
积性函数:对于所有
,满足
完全记性函数:对于所有
,满足
3.常见的积性函数
3.1 欧拉函数
表示
中与
互质的数的个数
,其中,
是
的不同因子集合
3.2 莫比乌斯函数
若
有平方因子,则
否则,
为
个质因数的乘积,则
3.3 幂函数
表示
特别的,有
3.4 单位函数
这个函数如果用群论的思想来理解的话就相当于一个幺元。
4.
——狄利克雷卷积
定义两数论函数
的
卷积
注意:卷积 中的 不是乘号,而是卷积的表示
4.1 卷积的性质
交换律:
结合律:
分配率:
单位元:
若
均为积性函数,则
仍为积性函数
具体的证明略去(太难写了)
4.2 常见的 卷积
我们在这里只列举常用的两个卷积式子
,即
,即
我们可以证明一下第二个式子(这个式子在后面很常用)
假设
有
个不同的质因子,则有
我们来理解一下这个式子。
首先,如果 的因数 中含有平方因子,则显然有 ,可以从和式中略去
那么对于一个数 (其中, 为 的质因子),根据 函数的定义,显然有
那么从 的 个质因子中任意选出 个之后,这些质因子的乘积设为 ,一定有 的值均为 ,并且这样的方案恰有 种。
它们的总和就得到了
显然有
根据二项式定理,我们最后得到
这个式子当且仅当 时等于 ,原式得证。
5.莫比乌斯反演
5.1 莫比乌斯反演的形式
如果有两个函数 ,满足
则它们也满足
反之亦然,即
5.2 莫比乌斯反演的证明
网上常见的证明方式都是用和式的变换来做的。我们今天就用
卷积来证明
设
我们将等式两侧同时卷上 ,得
(这一步我们刚才在常见的 卷积中证明过)
整理得
反过来同理
这样看来莫比乌斯反演的正确性就非常显然了,只要我们确信 即可
5.3 莫比乌斯反演的应用方式
事实上,我们的莫比乌斯反演通常是用来改变枚举的顺序,从而达到简化计算的目的。
举一个抽象的例子,我们已知
,如果说
是一个易于求解的函数,那么我们就可以通过反演的形式
来求得g的函数值。
6.莫比乌斯函数的常见套路
6.1改变和式的枚举顺序
已知
令 ,则原式得
6.2 由 到莫比乌斯反演
首先,我们常常会看到这样的一种表达
。
事实上,这个记号
可以等价于
例如:
已知
由于
此时,根据套路1,改变枚举顺序,首先枚举
这个式子就算是暴力枚举也可以在 的时间内完成,如果了解数论分块的小伙伴还可以在 的时间内求出答案。
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)。
简明题意:给定
,求
我们发现这个东西和套路2特别像,只不过区别是
还是
由于我们的
函数只能够表达
的情况,所以我们这里就要把原式转换为这种形式。
首先,我们先给出一个简单的式子:
令
,那么
这个东西特别显然,如果不能够很快接受的话可以想一下辗转相除法。
那么我们就可以把这个问题转换成为套路2了
套用套路2
所以我们就可以在 内解决问题
#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,时间复杂度。。。。总之小于
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 最后化出来的式子
但是这样显然还不够优秀啊,一次询问就是。。。大概接近 的时间复杂度, 次询问之后就会超时然后gg
所以我们看到了
这个神奇的变量(雾)
设 ,那么式子可以暂时先化一步
套路1,首先枚举T
23333,套路嘛,肯定是要一个套一个的。我们的套路4就很好的证明了这一点,套路123叠加就成为了套路4。
现在这个式子,我们只要能够预处理出 这一段就能够在 的时间内解决每一次询问了。
#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 暴力计算一类函数的前缀和
我们假设有这样一个函数 ,换句话说就是
那么计算 的暴力方案来了:
这个暴力算法的时间复杂度是多少呢?
为
回到之前的题目,我们要处理的式子就是
这个例子其实并不标准,因为只有质数我们才会加入处理。
所以我们的第一个循环并不需要枚举每一个可能的因数 ,只用枚举 范围内的质数即可。
由于 中的质数有 个,最后这个式子可以在 的时间内预处理完毕。
6.6 套路小结
这里就先写这些套路。事实上还有其他的的一些套路(比如有关 函数的套路, 的套路),但是基本上都可以从这5个基本套路中提炼出来。
当然,除了套路,对题目本身的敏感和对数学原理的了解程度都能够助你完成题目。
就到这里了。