数论 莫比乌斯反演
前置需求
数论分块
概念
对于一个形如
对于一个已知值
证明
摘自 OI-wiki。
应用
在莫反中通常除
if(n>m) swap(n,m);
int res=0;
for(int l=1,r;l<=n;l=r+1)
{
r=min(n/(n/l),m/(m/l));
res=/*code*/;
}
含 实现
例:求
思路是一样的,不过此时前缀和不再是相加而是相乘,需要用到快速幂和逆元。
由扩展欧拉定理可知:
if(n>m) swap(n,m);
int res=1;
for(int l=1,r;l<=n;l=r+1)
{
r=min(n/(n/l),m/(m/l));
res=1ll*res*qpow(1ll*f[r]*qpow(f[l-1],mod-2)%mod,1ll*(n/l)*(m/l)%(mod-1))%mod;
}
线性筛
莫反中必用到的知识点之一。
首先了解莫比乌斯函数:
你需要知道的,一是它是一个积性函数,二是怎么线性筛出它。
从定义能看出,
最简单的筛
int u[N],pri[N],tot;
bool vis[N];
void Wprepare()
{
u[1]=1;
for(int i=2;i<=N;i++)
{
if(!vis[i]) pri[++tot]=i,u[i]=-1;
for(int j=1;j<=tot;j++)
{
if(i*pri[j]>N) break;
vis[i*pri[j]]=1;
if(i%pri[j]==0) break;
u[i*pri[j]]=-u[i];
}
}
}
莫反通常多测,大多数我们需要在准备工作中多预处理一些式子里的其他函数值,有时候还要对其做求前缀和等一系列操作,这些都是因题而异,需要我们灵活应对。
一定的推式子能力
几个显然的很常用的式子转化:
默认
基础一点的:
提高一点的:
-
-
-
枚举分母,方便数论分块
令
含
具有结合律,并且适用于整除分块。
主体
由于蒟蒻不会卷积和其他的东西,所以只用到了最基础的一个反演公式
反演公式:
这是根据莫比乌斯函数的一个性质推出来的:
该性质证明如下:
摘自 OI-wiki。
如果你和我一样对这类式子没有什么太强烈的求知欲,那么食用方法如下:
- 根据题意,推出式子;
- 尽可能向
的方向推,然后使用反演公式; - 结合上面的式子转化,推出一个可以预处理出函数值,并能运用数论分块优化复杂度的式子,然后
AC开始调代码。
例题
做莫反的题我认为关键就在刷题,在不断地推式子中寻找共性,熟能生巧。
先推荐一篇学案,上面的推式子技巧都是循序渐进的,比较好。个人推荐一种学习方式:前几道较基础的题可以看着题解的式子一点一点理解,然后回过头来尝试自己独立完成推式子,再到不断优化筛法。
前几题式子部分会写的详细一点,可能导致篇幅冗长请见谅 qwq。
注:以下题未说明均默认
YY 的 GCD
这道题虽然在第一题的位置,但要考虑的其实还挺多的,综合了上面几乎所有的推式子技巧。
令
发现
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int Ratio=0;
const int N=1e7+5,M=1e7;
int u[N],pri[N],tot,yz[N],g[N];
void Wpre()
{
u[1]=1;
for(int i=2;i<=M;i++)
{
if(!yz[i]) pri[++tot]=i,u[i]=-1;
for(int j=1;j<=tot;j++)
{
if(i*pri[j]>M) break;
yz[i*pri[j]]=1;
if(i%pri[j]==0) break;
u[i*pri[j]]=-u[i];
}
}
for(int i=1;i<=tot;i++) for(int j=1;j<=M;j++)
{
if(pri[i]*j>M) break;
g[pri[i]*j]+=u[j];
}
for(int i=2;i<=M;i++) g[i]+=g[i-1];
}
int main()
{
Wpre();
int T,n,m;scanf("%d",&T);
while(T--)
{
scanf("%d%d",&n,&m);
if(n>m) swap(n,m);
long long res=0;
for(int l=1,r;l<=n;l=r+1)
r=min(n/(n/l),m/(m/l)),res+=1ll*(g[r]-g[l-1])*(n/l)*(m/l);
printf("%lld\n",res);
}
return Ratio;
}
约数个数和
个人感觉较简单的一道,要点在于求证
简证
摘自洛谷 Siyuan 的题解。
后面两坨看着就很熟悉是不是,没错就是数论分块的模板。这道题范围是
点击查看代码
#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int Ratio=0;
const int N=5e4+5,M=5e4;
int u[N],pri[N],tot,yz[N],sum[N];
ll g[N];
void Wpre()
{
u[1]=1;
for(int i=2;i<=M;i++)
{
if(!yz[i]) pri[++tot]=i,u[i]=-1;
for(int j=1;j<=tot;j++)
{
if(i*pri[j]>M) break;
yz[i*pri[j]]=1;
if(i%pri[j]==0) break;
u[i*pri[j]]=-u[i];
}
}
for(int i=1;i<=M;i++) sum[i]=sum[i-1]+u[i];
for(int i=1;i<=M;i++)
{
ll res=0;
for(int l=1,r;l<=i;l=r+1)
r=i/(i/l),res+=1ll*(r-l+1)*(i/l);
g[i]=res;
}
}
int main()
{
Wpre();
int T,n,m;scanf("%d",&T);
while(T--)
{
scanf("%d%d",&n,&m);
if(n>m) swap(n,m);
ll res=0;
for(int l=1,r;l<=n;l=r+1)
r=min(n/(n/l),m/(m/l)),res+=(sum[r]-sum[l-1])*g[n/l]*g[m/l];
printf("%lld\n",res);
}
return Ratio;
}
数表
都说是板子题,但我打了三天。
按照能把题做下去的思路,我们先不考虑
令
化到这一步,后面的部分已经可以提前处理了,此时考虑加入
令
那么我们完全可以离线下来做!讲所有询问按
插入值的复杂度近似于调和级数,树状数组更新复杂度为
点击查看代码
#include<bits/stdc++.h>
#define _(a,b) make_pair(a,b)
#define fi first
#define se second
typedef long long ll;
using namespace std;
const int Ratio=0;
const int N=1e5+5,M=1e5;
int u[N],pri[N],tot,yz[N],low[N];
int ans[N],v[N];
pair<int,int> o[N];
struct rmm{int n,m,a,id;}q[N];
bool cmp(rmm A,rmm B){return A.a<B.a;}
void Wpre()
{
u[1]=1,o[1]=_(1,1);
for(int i(2);i<=M;i++)
{
if(!yz[i]) pri[++tot]=i,u[i]=-1,low[i]=i+1,o[i]=_(i+1,i);
for(int j(1);j<=tot;j++)
{
if(i*pri[j]>M) break;
yz[i*pri[j]]=1;
if(i%pri[j]==0)
{
low[i*pri[j]]=low[i]*pri[j]+1;
o[i*pri[j]]=_(o[i].fi/low[i]*low[i*pri[j]],i*pri[j]);
break;
}
u[i*pri[j]]=-u[i];
low[i*pri[j]]=pri[j]+1;
o[i*pri[j]]=_(o[i].fi*o[pri[j]].fi,i*pri[j]);
}
}
sort(o+1,o+M+1);
}
void Wupd(int x,int k){for(;x<=M;x+=(x&-x)) v[x]+=k;}
int Wq(int x)
{
int res=0;
while(x) res+=v[x],x-=(x&-x);
return res;
}
int main()
{
Wpre();
int T;scanf("%d",&T);
for(int i(1);i<=T;i++) scanf("%d%d%d",&q[i].n,&q[i].m,&q[i].a),q[i].id=i;
sort(q+1,q+1+T,cmp);
int j=1;
for(int i(1);i<=T;i++)
{
while(j<=M&&o[j].fi<=q[i].a)
{
for(int k(o[j].se);k<=M;k+=o[j].se)
Wupd(k,o[j].fi*u[k/o[j].se]);
j++;
}
int res=0,n=q[i].n,m=q[i].m;
if(n>m) swap(n,m);
for(int l(1),r;l<=n;l=r+1)
r=min(n/(n/l),m/(m/l)),res+=(Wq(r)-Wq(l-1))*(n/l)*(m/l);
ans[q[i].id]=res;
}
for(int i(1);i<=T;i++) printf("%d\n",(ans[i]&(~(1<<31))));
return Ratio;
}
DZY Loves Math
有新函数,先不管它,推式子。
令
到这一步似乎就可以做了,不过后面的那坨
我们着眼于
然后再分两种情况:
一种是
另外就是
稍微理解下,然后再来分情况。若
整合一下,发现
其中
实现中,记录最小质因子的指数和它的指数次幂,转移时判断除去最小质因子后次小质因子的指数是否与之相同即可。
预处理
点击查看代码
#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int Ratio=0;
const int N=1e7+5,M=1e7;
int n,m;
int u[N],pri[N],tot,yz[N],Index[N],low[N],g[N];
void Wpre()
{
u[1]=1,g[1]=0;
for(int i(2);i<=M;i++)
{
if(!yz[i]) low[i]=pri[++tot]=i,u[i]=-1,Index[i]=g[i]=1;
for(int j(1);j<=tot;j++)
{
if(i*pri[j]>M) break;
yz[i*pri[j]]=1;
if(i%pri[j]==0)
{
Index[i*pri[j]]=Index[i]+1;
low[i*pri[j]]=low[i]*pri[j];
if(low[i]==i) g[i*pri[j]]=1;
else g[i*pri[j]]=Index[i/low[i]]==Index[i*pri[j]]?-g[i/low[i]]:0;
break;
}
u[i*pri[j]]=-u[i];
Index[i*pri[j]]=1;
low[i*pri[j]]=pri[j];
g[i*pri[j]]=Index[i]==1?-g[i]:0;
}
}
for(int i(2);i<=M;i++) g[i]+=g[i-1];
}
int main()
{
Wpre();
int T;scanf("%d",&T);
while(T--)
{
scanf("%d%d",&n,&m);
if(n>m) swap(n,m);
ll res=0;
for(int l(1),r;l<=n;l=r+1)
r=min(n/(n/l),m/(m/l)),res+=1ll*(g[r]-g[l-1])*(n/l)*(m/l);
printf("%lld\n",res);
}
return Ratio;
}
数字表格
从这道题开始就要接触含
令
上面那一坨已经不陌生了,拿下来化简。
令
括号外层的指数部分已经可以用数论分块解决了,考虑如何得到内层的
预处理部分调和级数复杂度约为
点击查看代码
#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int Ratio=0;
const int N=1e6+5,M=1e6;
const int mod=1e9+7;
int n,m;
int u[N],pri[N],tot,fib[N],invf[N],g[N];
bool yz[N];
int Wqp(ll x,int y)
{
ll res=1;
while(y){if(y&1) res=res*x%mod;x=x*x%mod;y>>=1;}
return res;
}
void Wpre()
{
u[1]=1,g[0]=g[1]=1,fib[1]=1,invf[1]=1;
for(int i(2);i<=M;i++)
{
fib[i]=(fib[i-1]+fib[i-2])%mod;
invf[i]=Wqp(fib[i],mod-2);
g[i]=1;
if(!yz[i]) pri[++tot]=i,u[i]=-1;
for(int j(1);j<=tot;j++)
{
if(i*pri[j]>M) break;
yz[i*pri[j]]=1;
if(i%pri[j]==0) break;
u[i*pri[j]]=-u[i];
}
}
for(int i(1);i<=M;i++)
{
if(!u[i]) continue;
for(int j(1);j<=M/i;j++)
g[i*j]=1ll*g[i*j]*(u[i]==1?fib[j]:invf[j])%mod;
}
for(int i(2);i<=M;i++) g[i]=1ll*g[i]*g[i-1]%mod;
}
int main()
{
Wpre();
int T;scanf("%d",&T);
while(T--)
{
scanf("%d%d",&n,&m);
if(n>m) swap(n,m);
ll res=1;
for(int l(1),r;l<=n;l=r+1)
r=min(n/(n/l),m/(m/l)),
res=1ll*res*Wqp(1ll*g[r]*Wqp(g[l-1],mod-2)%mod,1ll*(n/l)*(m/l)%(mod-1))%mod;
printf("%lld\n",(res+mod)%mod);
}
return Ratio;
}
于神之怒加强版
这题难点同样不在推式子,而是在后续函数的预处理。
令
考虑
考虑由积性函数的性质推出转移式:取任意一数
预处理是接近线性,数论分块处理询问,总复杂度
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int Ratio=0;
const int N=5e6+5,M=5e6;
const int mod=1e9+7;
int k;
int pri[N],tot,low[N],g[N];
bool yz[N];
int Wqp(int x,int y)
{
int res=1;
while(y){if(y&1) res=1ll*res*x%mod;x=1ll*x*x%mod;y>>=1;}
return res;
}
void Wpre()
{
g[1]=1;
for(int i(2);i<=M;i++)
{
if(!yz[i]) low[i]=pri[++tot]=i,g[i]=Wqp(i,k)-1;
for(int j(1);j<=tot;j++)
{
if(i*pri[j]>M) break;
yz[i*pri[j]]=1;
if(i%pri[j]==0)
{
low[i*pri[j]]=low[i]*pri[j];
if(low[i]==i) g[i*pri[j]]=1ll*(Wqp(i*pri[j],k)-Wqp(i,k)+mod)%mod;
else g[i*pri[j]]=1ll*g[i/low[i]]*g[low[i]*pri[j]]%mod;
break;
}
low[i*pri[j]]=pri[j];
g[i*pri[j]]=1ll*g[i]*g[pri[j]]%mod;
}
}
for(int i(2);i<=M;i++) g[i]=(g[i]+g[i-1])%mod;
}
int main()
{
int T;scanf("%d%d",&T,&k);
Wpre();
while(T--)
{
int n,m;scanf("%d%d",&n,&m);
if(n>m) swap(n,m);
int res=0;
for(int l(1),r;l<=n;l=r+1)
r=min(n/(n/l),m/(m/l)),
res=1ll*(res+1ll*(g[r]-g[l-1]+mod)%mod*(n/l)%mod*(m/l)%mod)%mod;
printf("%d\n",(res+mod)%mod);
}
return Ratio;
}
jzptab
第一道自己推出来的,%%% 5k 的引导。
发现
推到这一步我们完全可以记函数
拿出
最后注意坑点:模数是 1e8+9!
预处理复杂度
点击查看代码
#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int Ratio=0;
const int N=1e7+5,M=1e7;
const int mod=1e8+9;
int k;
int pri[N],tot;
ll s[N],g[N];
bool yz[N];
void Wpre()
{
g[1]=1,s[1]=1;
for(int i(2);i<=M;i++)
{
s[i]=1ll*i*(i+1)/2%mod;
if(!yz[i]) pri[++tot]=i,g[i]=(i-1ll*i*i%mod+mod)%mod;
for(int j(1);j<=tot;j++)
{
if(i*pri[j]>M) break;
yz[i*pri[j]]=1;
if(i%pri[j]==0)
{
g[i*pri[j]]=g[i]*pri[j]%mod;
break;
}
g[i*pri[j]]=g[i]*g[pri[j]]%mod;
}
}
for(int i(2);i<=M;i++) g[i]=(g[i]+g[i-1])%mod;
}
int main()
{
Wpre();
int T;scanf("%d",&T);
while(T--)
{
int n,m;scanf("%d%d",&n,&m);
if(n>m) swap(n,m);
ll res=0;
for(int l(1),r;l<=n;l=r+1)
r=min(n/(n/l),m/(m/l)),
res=(res+s[n/l]*s[m/l]%mod*(g[r]-g[l-1]+mod)%mod)%mod;
printf("%lld\n",res);
}
return Ratio;
}
Steps to One
CF1139D 用到一点莫反也是莫反。
题目大意:每次从
直接用期望式子推:
发现最后是个无穷等比数列,可以如下求值:
然后代入即可:
这道题范围很小,只有
点击查看代码
#include<bits/stdc++.h>
#define fo(x, y, z) for(register int (x) = (y); (x) <= (z); (x)++)
#define fu(x, y, z) for(register int (x) = (y); (x) >= (z); (x)--)
using namespace std;
typedef long long ll;
#define lx ll
inline lx qr()
{
char ch = getchar();lx x = 0 , f = 1;
for(;ch<'0'||ch>'9';ch = getchar()) if(ch == '-') f = -1;
for(;ch>= '0' && ch<= '9';ch = getchar()) x = (x<<3) + (x<<1) + (ch^48);
return x*f;
}
#undef lx
#define qr qr()
#define pii pair<int, int>
#define M_P(a, b) make_pair(a, b)
#define fi first
#define se second
#define P_B(x) push_back(x)
const int Ratio = 0;
const int N = 1e5 + 5, M = 1e5;
const int mod = 1e9 + 7;
int n;
int pri[N], tot, u[N];
bool yz[N];
ll ans = 1;
namespace Wisadel
{
ll Wqp(ll x, int y)
{
ll res = 1;
while(y){if(y & 1) res = res * x % mod; x = x * x % mod; y >>= 1;}
return res;
}
void Wpre()
{
u[1] = 1;
fo(i, 2, M)
{
if(!yz[i]) pri[++tot] = i, u[i] = -1;
fo(j, 1, tot)
{
if(i * pri[j] > M) break;
yz[i * pri[j]] = 1;
if(i % pri[j] == 0) break;
u[i * pri[j]] = -u[i];
}
}
}
short main()
{
// freopen(".in", "r", stdin) , freopen(".out", "w", stdout);
// freopen(".err", "w", stderr);
n = qr;
Wpre();
ll zc;
fo(i, 2, n)
zc = n / i, ans = (ans - u[i] * zc % mod * Wqp(n - zc, mod - 2) % mod + mod) % mod;
printf("%lld\n", ans);
return Ratio;
}
}
signed main(){return Wisadel::main();}
其他例题会逐渐更新。
有道理
数论既然上不了首页,那就推首歌吧
- 有道理——李荣浩
太理智的消费 对不对
成夜不睡
没一句说的对 也很对
这个年岁
有爱的人不追 还斗嘴
显得般配
有时也为自己感到惭愧
穿的也不算贵 黑白灰
尽量自费
被自己往里推 动动嘴
也能后退
总会被动吃亏 不防备
深知有罪
我现在要开始表现伤悲和承认理亏
人文和法规
其实都算是有理想啊
都是废寝食忘啊
有深度有主张啊
难免会累会慌
难免也会迷茫
太理智的消费 对不对
成夜不睡
没一句说的对 也很对
这个年岁
有爱的人不追 还斗嘴
显得般配
有时也为自己感到惭愧
穿的也不算贵 黑白灰
尽量自费
被自己往里推 动动嘴
也能后退
总会被动吃亏 不防备
深知有罪
我现在要开始表现伤悲和承认理亏
人文和法规
其实都算是有理想啊
都是废寝食忘啊
有深度有主张啊
难免会累会慌
难免也会迷茫
其实都算是有理想啊
都是废寝食忘啊
有深度有主张啊
难免会累会慌
难免也会迷茫
其实都算是有理想啊
都是废寝食忘啊
有深度有主张啊
难免会累会慌
难免也会迷茫
其实都算是有理想啊
都是废寝食忘啊
有深度有主张啊
难免会累会慌
难免也会迷茫
其实都算是有理想啊
制作不易,如有帮助,请推荐支持,感谢!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探