莫比乌斯反演学习笔记
在Imagine大佬的博客帮助下整了一周莫某某反演,总结一下学的一些新东西和我做的水题的一些小套路;
本篇文章没有对反演进行证明,只是记录了一些理解与做题时遇到的感觉挺有用的小技巧
首先是莫比乌斯函数\(\mu\)
有\(\sum_{d|n}\mu(d)=[n=1]\)
是积性函数
可以用线性筛在\(O(n)\)的时间内得到
void Prepare() {
mu[1]=1;
for(int i=2;i<=MAX;++i) {
if(!sign[i]) pri[++cnt]=i,mu[i]=i-1;
for(int j=1;i*pri[j]<=MAX;++j) {
sign[i*pri[j]]=true;
if(i%pri[j]==0) {
mu[i*pri[j]]=0;
break;
}
else mu[i*pri[j]]=-mu[i];
}
}
}
丢一道题BZOJ2440,容斥的时候也可以考虑一下莫比乌斯函数;
然后是狄利克雷卷积
有两个数列函数\(f\),\(g\),它们的狄利克雷卷积定义为\((f*g)(n)=\sum_{d|n}f(d)*g(\frac{n}{d})\);
若\(f\)和\(g\)都是积性函数,则它们的狄利克雷卷积也为积性函数;
狄利克雷卷积满足交换律、结合律,对加法满足分配律
最后是莫比乌斯反演
先说反演,反演在干个什么事呢,我的理解就是给了你一些已知量,然后未知量和已知量直接有某种运算关系,然后通过已知量反解出未知量的过程就是反演;
而莫比乌斯反演也是干这样的事,若函数\(f\),\(g\)满足这样的关系:
这个的具体证明也是大力展开式子,具体可看其他人博客(逃
有了这两个关系式之后我们能干些啥事呢?
就像刚刚所说,我们可以通过很快算出其中一个函数的取值,再通过莫比乌斯反演反解出另一个函数,从而大大降低复杂度;
(接下来如果式子中同时出现了\(n\)和\(m\),默认\(n\leq m\));
来看道例题BZOJ1101
这道题需要求的就是\(\sum_{i=1}^n\sum_{j=1}^m[gcd(i,j)=d]\)
设函数\(g(d)=\sum_{i=1}^n\sum_{j=1}^m[gcd(i,j)=d]\),我们发现主要的困难点就是这个\(g(d)\)感觉起码要花\(O(n^2)\)的代价去算这个东西,有多组询问这样复杂度就变成了\(O(n^3)\)以上了,怎么办呢,想起莫比乌斯反演,如果我们能找一个满足反演式子,切算起来复杂度很低的式子,我们是不是就能够很快解决这个问题呢?这题的这个式子很好找而且很套路,设\(f(k)=\sum_{i=1}^n\sum_{j=1}^m[k|gcd(i,j)]\),\(f(k)\)就相当于找\(gcd(i,j)\)是\(k\)的倍数的\(i,j\)对数,这样是可以\(f\)直接算出来的,有\(f(k)=\left \lfloor\frac{n}{k}\right\rfloor\left\lfloor\frac{m}{k}\right\rfloor\),那么进行反演
有$$f(k)=\sum_{k|d}g(d)$$
则$$g(d)=\sum_{d|k}\mu(\frac{k}{d})f(k)$$$$g(d)=\sum_{i=1}{\left\lfloor\frac{n}{d}\right\rfloor}\mu(i)f(id)$$$$g(d)=\sum_{i=1}\right\rfloor}\mu(i)\left \lfloor\frac{n}{id}\right\rfloor\left\lfloor\frac{m}{id}\right\rfloor$$现在我们只需要O(n)的时间,便能完成一次询问了,但是还不够,因为这道题是多组询问,这个时候就要用的一个非常常用的小技巧数论分块了;
当式子里出现这么个东西\(\sum_{i=1}^n\left \lfloor\frac{n}{i}\right\rfloor\),考虑\(\left \lfloor\frac{n}{i}\right\rfloor\)的取值,很容易看出当\(1\leq i\leq \sqrt n\)的时候\(\left \lfloor\frac{n}{i}\right\rfloor\)最多有\(\sqrt n\)个取值,当\(\sqrt n\leq i\leq n\)的时候,\(\left \lfloor\frac{n}{i}\right\rfloor\)也最多只有\(\sqrt n\)个取值,且这些区间是连续的,那么我们就可以一次性把所有取值相同的\(i\)一起算完,比如\(i=d\)的时候是一个新取值区间的左端点,右端点便是\(\left \lfloor\frac{n}{\left \lfloor\frac{n}{i}\right\rfloor}\right\rfloor\),那么在\(O(\sqrt n)\)的时间内我们就能得到\(\sum_{i=1}^n\left \lfloor\frac{n}{i}\right\rfloor\)的答案;
\(\left \lfloor\frac{n}{id}\right\rfloor\left\lfloor\frac{m}{id}\right\rfloor\)同理,只是注意在取右端点的时候要取更靠左的那一个,详细看代码;
#include<bits/stdc++.h>
#define Fst first
#define Snd second
#define RG register
#define mp make_pair
#define mem(a,b) memset(a,b,sizeof(a))
using namespace std;
typedef long long LL;
typedef long double LD;
typedef unsigned int UI;
typedef unsigned long long ULL;
template<typename T> inline void read(T& x) {
char c = getchar();
bool f = false;
for (x = 0; !isdigit(c); c = getchar()) {
if (c == '-') {
f = true;
}
}
for (; isdigit(c); c = getchar()) {
x = x * 10 + c - '0';
}
if (f) {
x = -x;
}
}
template<typename T, typename... U> inline void read(T& x, U& ... y) {
read(x), read(y...);
}
const int MAX=5e4,N=MAX+10;
int cnt,Q;
int pri[N],mu[N];
bool sign[N];
void Prepare() {
mu[1]=1;
for(int i=2;i<=MAX;++i) {
if(!sign[i]) pri[++cnt]=i,mu[i]=-1;
for(int j=1;i*pri[j]<=MAX;++j) {
sign[i*pri[j]]=true;
if(i%pri[j]==0) {
mu[i*pri[j]]=0;
break;
}
else mu[i*pri[j]]=-mu[i];
}
}
for(int i=1;i<=MAX;++i) mu[i]+=mu[i-1];
}
LL Calc(int n,int m,int d) {
if(n>m) swap(n,m);
n/=d; m/=d;
LL res=0;
for(int i=1;i<=n;++i) {
int p=min(n/(n/i),m/(m/i));
res+=1ll*(mu[p]-mu[i-1])*(n/i)*(m/i);
i=p;
}
return res;
}
int main() {
#ifdef rua
freopen("GG.out","w",stdout);
#endif
Prepare();
read(Q);
while(Q--) {
int n,m,d; read(n,m,d);
printf("%lld\n",Calc(n,m,d));
}
return 0;
}
接下来的题每道都会带一些小技巧,比如式子的替换或者一些函数怎么拆开;
接下来的除法,除非做特殊说明否则都是向下取整(向下取整符号真的难打
BZOJ2154
这题是叫你求\(\sum_{i=1}^n\sum_{j=1}^mlcm(i,j)\)
看到\(lcm\)往往要拆成\(gcd\),所以式子化为$$\sum_{i=1}n\sum_{j=1}m\frac{ij}{gcd(i,j)}$$
然后提\(gcd\)出来
再把\(d\)提出来方便分析
有这个\(ij\)在后面卡着我们不好反演,这时想起一个好东西\(\sum_{d|n}=[n=1]\),我们正好把后面的\(gcd\)换了;
这也是个常用技巧,设\(T=dx\),我们变换下标,把它提到前面去看看怎么样$$\sum_{T=1}nSum(\frac{n}{T},\frac{m}{T})\sum_{d|T}d*\mu(\frac{T}{d})*(\frac{T}{d})2$$
现在我们来观察一下后面这坨东西
\(d\)和\((\frac{T}{d})^2\)都可以看作幂函数\(id^k(n)=n^k\),完全积性,\(\mu\)函数,积性函数;
\( \)
这坨东西好像可以筛;
设$$g(n)=\sum_{d|n}d\mu(\frac{n}{d})(\frac{n}{d})^2$$
现在考虑\(n=1\)时\(g(n)=1\)
当\(n\)为质数时,\(d\)只有\(1\)和\(n\)两种取值,此时\(g(n)=n-n^2\)
当\(n\)不为质数时,考虑唯一分解定理\(g(n)=\prod_{p_i为n的质因子}^tg(p_i^{c_i})\)
考虑线性筛的两种情况,第一种\(i\%pri[j]!=0\)时,因为\(g\)是积性函数,\(g(i*pri[j])=g(i)*g(pri[j])\)
否则考虑新进来一个质数\(p\)会增加怎样的新因子,因为如果\(\mu(d)\)的\(d\)中含有二次以上的因子,必然会等于\(0\),此时发现原来那些有效的因子\(d\),乘上一个\(p\)后继续有效,其他的\(d\)都因为\(\frac{n}{d}\)含有了二次以上的因子导致\(\mu(d)=0\)可以不用考虑了,所以此时的\(g(i*pri[j])=g(i)*pri[j]\);
所以我们可以用线性筛先预处理出所有的\(g(T)\),现在式子变成$$\sum_{T=1}^nSum(\frac{n}{T},\frac{m}{T})g(T)$$
再加上除法分块,我们便能在\(O(\sqrt n)\)的时间内完成一次询问了;
#include<bits/stdc++.h>
#define Fst first
#define Snd second
#define RG register
#define mp make_pair
#define mem(a,b) memset(a,b,sizeof(a))
using namespace std;
typedef long long LL;
typedef long double LD;
typedef unsigned int UI;
typedef unsigned long long ULL;
template<typename T> inline void read(T& x) {
char c = getchar();
bool f = false;
for (x = 0; !isdigit(c); c = getchar()) {
if (c == '-') {
f = true;
}
}
for (; isdigit(c); c = getchar()) {
x = x * 10 + c - '0';
}
if (f) {
x = -x;
}
}
template<typename T, typename... U> inline void read(T& x, U& ... y) {
read(x), read(y...);
}
const int MAX=1e7,N=MAX+10,P=20101009;
int cnt,T;
int F[N],G[N],pri[N];
bool sign[N];
void Prepare() {
F[1]=1;
for(int i=2;i<=MAX;++i) {
if(!sign[i]) pri[++cnt]=i,F[i]=(i-1ll*i*i%P+P)%P;
for(int j=1;i*pri[j]<=MAX;++j) {
sign[i*pri[j]]=true;
if(i%pri[j]==0) {
F[i*pri[j]]=1ll*F[i]*pri[j]%P;
break;
}
else F[i*pri[j]]=1ll*F[i]*F[pri[j]]%P;
}
}
for(int i=1;i<=MAX;++i) F[i]=(F[i]+F[i-1])%P,G[i]=(1ll*i*(i+1)/2ll)%P;
}
int Calc(int n,int m) {
if(n>m) swap(n,m);
int res=0;
for(int i=1;i<=n;++i) {
int p=min(n/(n/i),m/(m/i));
res=(res+1ll*((F[p]-F[i-1])%P+P)%P*G[n/i]%P*G[m/i]%P)%P;
i=p;
}
return res;
}
int main() {
#ifdef rua
freopen("GG.out","w",stdout);
#endif
Prepare();
T=1;
while(T--) {
int n,m; read(n,m);
printf("%d\n",Calc(n,m));
}
return 0;
}
BZOJ2005
这题抽象一下之后,我们的重点就是求\(\sum_{i=1}^n\sum_{j=1}^mgcd(i,j)\)
本题可以用上面那个\(O(\sqrt n)\)的反演求\(gcd=d\)的个数暴力去跑\(n\)次得到答案;
但是这里要介绍另外一种方法,因为这个方法经常用于式子的替换上;
这里不再是,求个数了,而是具体的和,于是可以用的欧拉函数\(\varphi\)的一个性质$$n=\sum_{d|n}\varphi(d)$$
于是式子变成了这样$$\sum_{i=1}n\sum_{j=1}m\sum_{d|i,d|j}\varphi(d)$$
很套路的把\(d\)提到前面来$$\sum_{d=1}^n\varphi(d)\frac{n}{d}\frac{m}{d}$$
数论分块一下,跑的比反演不知道快到哪里去了了;
#include<bits/stdc++.h>
#define Fst first
#define Snd second
#define RG register
#define mp make_pair
#define mem(a,b) memset(a,b,sizeof(a))
using namespace std;
typedef long long LL;
typedef long double LD;
typedef unsigned int UI;
typedef unsigned long long ULL;
template<typename T> inline void read(T& x) {
char c = getchar();
bool f = false;
for (x = 0; !isdigit(c); c = getchar()) {
if (c == '-') {
f = true;
}
}
for (; isdigit(c); c = getchar()) {
x = x * 10 + c - '0';
}
if (f) {
x = -x;
}
}
template<typename T, typename... U> inline void read(T& x, U& ... y) {
read(x), read(y...);
}
const int MAX=1e5,N=MAX+10;
int cnt;
int pri[N];
bool sign[N];
LL phi[N];
void Prepare() {
phi[1]=1;
for(int i=2;i<=MAX;++i) {
if(!sign[i]) pri[++cnt]=i,phi[i]=i-1;
for(int j=1;i*pri[j]<=MAX;++j) {
sign[i*pri[j]]=true;
if(i%pri[j]==0) {
phi[i*pri[j]]=phi[i]*pri[j];
break;
}
else phi[i*pri[j]]=phi[i]*phi[pri[j]];
}
}
for(int i=1;i<=MAX;++i) phi[i]+=phi[i-1];
}
int main() {
#ifdef rua
freopen("GG.in","r",stdin);
#endif
Prepare();
int n,m; read(n,m);
if(n>m) swap(n,m);
LL res=-1ll*n*m;
for(int i=1;i<=n;++i) {
int p=min(n/(n/i),m/(m/i));
res+=(phi[p]-phi[i-1])*(n/i)*(m/i)*2;
i=p;
}
printf("%lld\n",res);
return 0;
}
换\(\varphi\)的时候往往会把一些比较复杂的反演变成非常简单的筛\(\varphi\)
推荐一些题,因为做的难度中等偏下,可能会比较水;
BZOJ2694&BZOJ4659:两道题一样的,式子推出来复杂度不够优秀不能支持多组询问,需要用的上面所说的变换下标;
BZOJ4407:同样是变换下标线性筛;
BZOJ3529:离线+树状数组;
BZOJ4816:累乘的反演,其实累乘就是在质数上相加;
LuoguP3768:本来挺复杂度反演,通过换\(\varphi\)变成了比较水的裸杜教筛;
暂时先写那么多;