这个算是竞赛中用到的组合数学里最多的知识点,也是比较难理解的,在被将近一个星期的折磨后总算是理解了一些。
其实莫比乌斯反演就只有两个重要的公式,而且用到的最多的也只有一个(其实都差不多),但是需要一些预备知识。
莫比乌斯函数,质数线性筛,整除分块,积性函数以及一些其他的数论知识,下面会将其中一部分重要的算法,
目录
1.莫比乌斯函数
莫比乌斯函数没有什么好写的,就是一个需要记住的函数,具体的证明以及内容请参考:百度百科
| n=1; 若n无平方因子,即n=p1*p2*p3*p4...*pn(都不同) others
|
2.莫比乌斯函数的线性筛
其实就是预处理求出莫比乌斯函数,通常还会求出μ函数的前缀和,其实和质数线性筛的差不多。
代码如下(来自B站UestcAcm的视频):
int prime[MAXN], prime_tot;//prime[MAXN]用于保存质数,prime_tot是质数的个数;
bool prime_tag[MAXN];//标记是否为质数,false表示是质数,true表示合数;
int mu[MAXN];//保存n的莫比乌斯函数值;
void pre_calc(int lim){
mu[1] = 1;
for (int i = 2; i <= lim; ++i) {
if(!prime_tag[i]) {
prime[++prime_tot] = i;
mu[i] = -1;
//质数的莫比乌斯函数值必为-1;
}
for (int j = 1; j <= prime_tot; ++j) {
if(i * prime[j] > lim)break;
prime_tag[i * prime[j] ] = true;
//质数的倍数必是合数
if(i % prime[j] == 0) {
//如果 i能被质数整除 说明 i中必有同一个质数因子
//那么 i*prime[j] 就必有平方因子
mu[i * prime[j] ] = 0;
break;
}
else {
mu[i * prime[j] ] = -mu[i];
}
}
}
}
3.整除分块
这个算是一个技巧吧,在莫比乌斯反演里面用的比较多。
通常用来快速解决这类问题,通常我们求解的方法是O(n)的时间复杂度,但是我们可以发现在求解时有一部分的值是一样的。
拿10来举个例子吧,i=1时值为10,i=2时值是5,i=3时值是3,i=4时值是2,i=5时值是2,i=6,7,8,9,10时值是1。
我们会发现在枚举时,会花大部分的时间去求一些重复的值,把这部分时间优化一下是整除分块。
下面引用一部分来自网上大佬的讲解(整除分块)。
首先观察这个式子,找几个特殊值代入n=5时,sum=5+2+1+1+1
可以发现的是:(这里给的例子并不明显,其实应该找一个大的n来代入才直观,读者可以自行尝试)
对于单一的⌊n/i⌋,某些地方的值是相同的,并且呈块状分布
通过进一步的探求规律与推理以及打表与瞎猜,我们可以惊喜的发现一个规律,这些块状分布的值是有规律的
对于一个块,假设它的起始位置的下标为l,那么可以得到的是,它的结束位置的下标为
如果实在看的有点懵逼,可以继续采用代入特殊值的方法,验证一下上方的规律,用程序表现出来即为
//l为块的左端点,r为块的右端点
r=n/(n/l)
具体代码实现为(要特判为0的情况):
long long ans=0;
int i=1;
while(i<=n){
j=n/(n/i);
i=j+1;
// do something
}
4.积性函数和狄利克雷卷积
积性函数分为完全积性函数和积性函数
积性函数:对于函数f(n),若满足对任意互质的数字a,b,a∗b=n且f(n)=f(a)*f(b),那么称函数f为积性函数。
完全积性函数:对于函数f(n),若满足对任意的数字a,b,a∗b=n且f(n)=f(a)*f(b),那么称函数f为积性函数。
狄利克雷卷积是函数之间的运算。
狄利克雷卷积:
狄利克雷卷积满足很多性质:
交换律:f∗g=g∗f
结合律:(f∗g)∗h=f∗(g∗h)
一些常用的积性函数(来自yyb的博客:莫比乌斯反演):
μ 莫比乌斯函数
这个怎么筛应该都会吧
φ 欧拉函数
怎么筛应该也很明显吧。
d 约数个数
这个怎么筛?
考虑唯一分解定理:
x=
那么d(x)=
记录一下最小质因子的个数
每次就先把原来的除掉,再把+1 后的个数乘上就好啦
σ 约数和
还是唯一分解定理
x=
σ(x)=
记录一下最小质因子的上面那个式子的和
以及这个因子的ai次幂
每次也是先除掉再乘上新的
ak的k 次幂
把这个东西写进来,只是为了提醒一下
ak这种东西是一个完全积性函数,也是可以丢进去筛的
inv 乘法逆元
没啥,一样的,乘法逆元也是完全积性函数
蛤,我知道可以O(n)递推
只是写一下而已,具体请参考线性筛,积性函数,狄利克雷卷积,常见积性函数的筛法
还有一些比较重要的请参考:UestcAcm的莫比乌斯反演ppt
5.莫比乌斯反演
终于到最后的莫比乌斯反演了,花了一节实验课把预备知识写完,剩下的都是重点了!!!
莫比乌斯反演公式(证明请参考:组合计数和反演):
理论知识不多,但是用处灰常大!!!!
6.习题演练
知识点讲完了,该讲一下习题了。
题意:给你a,b,d,让你求出的值。
算是最经典的莫比乌斯反演题。
设 f(d) 为 ,
根据莫比乌斯反演定理,,故g(n)为gcd(i,j)==d和d的倍数的个数。
,易知g(d)=
。
那么我们由莫比乌斯反演可得
我们预处理求出μ的前缀和并且用数论分块求就能保证时间复杂度是
代码如下:
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<bits/stdc++.h>
#define rp(i,s, t) for (i = s; i <= t; i++)
#define RP(i,s, t) for (i = t; i >= s; i--)
#define ll long long
#define ull unsigned long long
using namespace std;
const int manx=1e5+7;
int visited[manx],prime[manx],tot=0;
ll mu[manx];
ll sum[manx];
inline int read()
{
int x=0,t=1;char ch=getchar();
while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
if(ch=='-')t=-1,ch=getchar();
while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
return x*t;
}
void init(){
memset(visited,0,sizeof(visited));
mu[1]=1;
int i,j;
rp(i,2,manx-1){
if(visited[i]==0){
mu[i]=-1;
visited[i]=1;
prime[tot++]=i;
}
rp(j,0,tot-1){
if(prime[j]*i>=manx) break;
visited[prime[j]*i]=1;
if(i%prime[j]==0){
mu[i*prime[j]]=0;
break;
}
else{
mu[i*prime[j]]=-mu[i];
}
}
}
rp(i,1,manx-1) sum[i]=sum[i-1]+mu[i];
}
int main(){
int T;
T=read();
init();
while(T--){
int a=read(),b=read(),d=read();
a/=d;
b/=d;
if(a>b) swap(a,b);
int i=1;
ll ans=0;
while(i<=a){//数论分块
int j=min(a/(a/i),b/(b/i));
ans+=(sum[j]-sum[i-1])*(a/i)*(b/i);
i=j+1;
}
printf("%lld\n",ans);
}
return 0;
}
题意同上。
和上面很类似的一道题,只不过需要处理一下重复的情况(这里把gcd(7,5)和gcd(5,7)看成同一种情况)
不过这个题时间限度很低,因此我们可以直接容斥来求,时间复杂度为O(n)
最后我们去掉重复的情况(即最小部分的和的一半除以2)。
#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#define ll long long
#define rp(i,s,t) for(i=s;i<=t;i++)
#define Rp(i,s,t) for(i=t;i>=s;i--)
using namespace std;
const int manx=1e5+7;
int mu[manx],prime[manx],visited[manx];
ll g[manx];
int tot=0;
void init(){//预处理求出μ函数
tot=0;
memset(visited,0,sizeof(visited));
mu[1]=1;
int i,j;
rp(i,2,manx){
if(!visited[i]){
prime[tot++]=i;
mu[i]=-1;
visited[i]=1;
}
rp(j,0,tot-1){
if(prime[j]*i>manx) break;
visited[prime[j]*i]=1;
if(i%prime[j]==0){
mu[i*prime[j]]=0;
break;
}
else{
mu[i*prime[j]]=-mu[i];
}
}
}
}
int main(){
int T;
int a,b,c,d,k;
scanf("%d",&T);
init();
int kas=0;
while(T--){
scanf("%d%d%d%d%d",&a,&b,&c,&d,&k);
if(k==0){//特判等于0的情况
printf("Case %d: 0\n",++kas);
continue;
}
if(b>d) swap(b,d);
b/=k;
d/=k;
ll ans=0,res=0;
int i;
rp(i,1,b) g[i]=1ll*(b/i)*(d/i);
rp(i,1,b) ans+=1ll*mu[i]*g[i];
rp(i,1,b) res+=1ll*mu[i]*(b/i)*(b/i);//算出公共部分并去掉
printf("Case %d: %lld\n",++kas,ans-res/2);//利用容斥得到答案
}
return 0;
}
求的值。
容斥定理的应用,不难想到我们可以转换成求的值。
至于为什么呢?我们可以先进行一次转换:,之后代入原式。
带入后式子变成了。
整理可得:。
再进行一次变换:,然后代入上式可得:
整理可得:
剩下的就和上一个题的差不多了。
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<bits/stdc++.h>
#define rp(i,s, t) for (i = s; i <= t; i++)
#define RP(i,s, t) for (i = t; i >= s; i--)
#define ll long long
#define ull unsigned long long
using namespace std;
inline int read()
{
int x=0,t=1;char ch=getchar();
while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
if(ch=='-')t=-1,ch=getchar();
while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
return x*t;
}
inline void write(int x)
{
char F[200];
int tmp=x>0?x:-x ;
if(x<0)putchar('-') ;
int cnt=0 ;
while(tmp>0)
{
F[cnt++]=tmp%10+'0';
tmp/=10;
}
while(cnt>0)putchar(F[--cnt]) ;
}
const int N=1e5+7;
int tot,visited[N],prime[N],mu[N];
ll sum[N];
int k;
void init(int n)//预处理求出莫比乌斯函数的前缀和
{
tot=0;
memset(visited,0,sizeof visited);
mu[1]=1;
int i,j;
rp(i,2,n){
if(!visited[i]){
visited[i]=1;
prime[tot++]=i;
mu[i]=-1;
}
rp(j,0,tot-1){
if(i*prime[j]>n) break;
visited[i*prime[j]]=1;
if(i%prime[j]==0){
mu[i*prime[j]]=0;
break;
}
else
mu[i*prime[j]]=-mu[i];
}
}
rp(i,1,n) sum[i]=sum[i-1]+mu[i];
}
ll solve(int x,int y){
if(x>y) swap(x,y);
x/=k,y/=k;
int i=1;
ll ans=0;
while(i<=x){//数论分块
int j=min(x/(x/i),y/(y/i));
ans+=(sum[j]-sum[i-1])*1ll*(x/i)*(y/i);
i=j+1;
}
return ans;
}
int main(){
int T=read();
init(100000);
while(T--)
{
int a=read(),b=read(),c=read(),d=read();
k=read();
ll ans=solve(b,d)-solve(b,c-1)-solve(a-1,d)+solve(a-1,c-1);//容斥定理
printf("%lld\n",ans);
}
return 0;
}
求的值。
这个题如果我们直接求出所有质数,然后在枚举的话肯定会超时,所有我们想能不能再化简一下原来的公式。
调用前面的式子可以进行变换得到:
那么令T=pi,代入上式可得:
这一步我们可以理解为从枚举p变成了枚举p的倍数从而间接实现枚举p(当且仅当p整除T时,对T才会有贡献)。
下一步是因为后面的T对于枚举p的求和时是常量,所以可以提出了。
然后我们可以先预处理求出后面的前缀和,即,之后再分块求解。
我们可以在筛出质数后,求出的前缀和,然后就可以用O(T
)的时间复杂度求出答案。
#include<iostream>
#include<algorithm>
#include<bits/stdc++.h>
#include<cmath>
#include<cstring>
#define rp(i,s, t) for (i = s; i <= t; i++)
#define RP(i,s, t) for (i = t; i >= s; i--)
#define ll long long
#define ull unsigned long long
using namespace std;
inline int read()
{
int x=0,t=1;char ch=getchar();
while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
if(ch=='-')t=-1,ch=getchar();
while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
return x*t;
}
inline void write(int x)
{
char F[200];
int tmp=x>0?x:-x ;
if(x<0)putchar('-') ;
int cnt=0 ;
while(tmp>0)
{
F[cnt++]=tmp%10+'0';
tmp/=10;
}
while(cnt>0)putchar(F[--cnt]) ;
}
const int N=1e7+5;
int mu[N],prime[N],tot,visited[N],temp[N];
ll sum[N];
void init(int n)
{
mu[1]=1;
int i,j;
rp(i,2,n){
if(!visited[i]){
prime[tot++]=i;
visited[i]=1;
mu[i]=-1;
}
rp(j,0,tot-1){
if(i*prime[j]>n) break;
visited[i*prime[j]]=1;
if(i%prime[j]==0){
mu[i*prime[j]]=0;
break;
}
else
mu[i*prime[j]]=-mu[i];
}
}
rp(j,0,tot-1){
rp(i,1,n){
if(i*prime[j]>n) break;
temp[i*prime[j]]+=mu[i];//先求出所有的μ(T/t)
}
}
rp(i,1,n)//预处理求出前缀和
sum[i]=sum[i-1]+1ll*temp[i];
}
int main(){
int T=read();
init(10000000);
while(T--){
int n=read(),m=read();
if(n>m) swap(n,m);
int i=1,j;
ll ans=0;
while(i<=n){
int j=min(n/(n/i),m/(m/i));
ans+=1ll*(sum[j]-sum[i-1])*(n/i)*(m/i);
i=j+1;
}
cout<<ans<<endl;
}
return 0;
}
和上面的题一样,不过我们需要在求出μ的时候同时处理前缀和(相比上面的少了个循环,即常数变小了)。
如果和上面代码一样会T,有点卡常。
#include <iostream>
#include <algorithm>
#include <bits/stdc++.h>
#include <cmath>
#include <cstring>
#define rp(i, s, t) for (i = s; i <= t; i++)
#define RP(i, s, t) for (i = t; i >= s; i--)
#define ll long long
#define ull unsigned long long
using namespace std;
inline int read()
{
int x = 0, t = 1;
char ch = getchar();
while ((ch < '0' || ch > '9') && ch != '-')
ch = getchar();
if (ch == '-')
t = -1, ch = getchar();
while (ch <= '9' && ch >= '0')
x = x * 10 + ch - 48, ch = getchar();
return x * t;
}
inline void write(int x)
{
char F[200];
int tmp = x > 0 ? x : -x;
if (x < 0)
putchar('-');
int cnt = 0;
while (tmp > 0)
{
F[cnt++] = tmp % 10 + '0';
tmp /= 10;
}
while (cnt > 0)
putchar(F[--cnt]);
}
const int N = 1e7 + 5;
int mu[N], prime[N],tot,visited[N], temp[N];
ll sum[N];
void init(int n)
{
int i,j;
memset(visited,0,sizeof(visited));
tot = 0;
mu[1] = 1;
temp[1] = sum[1] = 0;
rp(i,2,n)
{
if (!visited[i])
{
prime[tot++] = i;
mu[i] = -1;
temp[i] = mu[1];
}
rp(j,0,tot-1){
if(i*prime[j]>n) break;
visited[i * prime[j]] = 1;
if (i%prime[j]==0){
mu[i * prime[j]] = 0;
temp[i * prime[j]] = mu[i];
break;
}
else{
mu[i * prime[j]] = -mu[i];
temp[i * prime[j]] = mu[prime[j]] * temp[i] + mu[i];
}
}
}
//注意这里会卡常,所以在线性筛的时候处理就行了。
// rp(j,0,tot-1){
// rp(i,1,n){
// if(i*prime[j]>n) break;
// temp[i*prime[j]]+=mu[i];//先求出所有的μ(T/t)
// }
// }
rp(i,1,n) sum[i]=sum[i-1]+temp[i];
}
int main()
{
init(10000000);
int n = read();
int i = 1, j;
ll ans = 0;
while (i <= n)
{
int j = n / (n / i);
ans += 1ll * (sum[j] - sum[i - 1]) * (n / i) * (n / i);
i = j + 1;
}
printf("%lld\n", ans);
return 0;
}
//27493351185725
其实光会莫比乌斯反演还完全不够,往往要用到min_25筛和杜教筛进行进一步的优化,但是这些还不是很会,等这几天学会了再来补这些算法。
通常这类题型都是:莫比乌斯反演推式子,杜教筛(或者min_25筛)求前缀和。一点也不浪费呀!!!所以说要学就只能都学。