莫比乌斯反演入门

来自这位大佬视频的整理

反演基础

先整理几个重要的数论函数。

1.莫比乌斯函数

μ(n)n=1时取1 ,当n存在平方因子的时候取0 ,否则取(1)k,其中kn所含的质因子数量。

2.欧拉函数

ϕ(n)=d=1n[gcd(d,n)=1],就是小于等于n且与n互质的数字的数量。

3.因子函数

σ(n)表示 n的所有的因子的和
σ(n)=d|nd

还有一个小的ϵ(n)=[n=1]

前两个都是积性函数。

接下来是几个重要的反演结论

ϵ(n)=d|nμ(d)

n=d|nϕ(d)

σ(n)=d|nd

我们使用这三个式子的目的是把目标式转化为可求的东西。比如莫比乌斯函数,或者是因子和之类的东西。

来看看例题就知道了。这些例题其实就是基本模型。

例一、求i=1nj=1m[gcd(i,j)=1]的值

其实就是i=1nj=1mϵ(gcd(i,j)),那么根据第一个式子,可以变化为i=1nj=1md|gcd(i,j)μ(d)
而,d|gcd(i,j)有可以表述为d|id|j,那么我们就先枚举d,然后再枚举所有的ij,假如 d|id|j同时成立再加上答案。
那么式子就会变为dmin(n,m)μ(d)i=1nj=1m([d|i][d|j])
再然后,可以发现d|i这一项和最后的一重循环j一点关系没有,于是可以把式子转化为dmin(n,m)μ(d)(i=1n[d|i])(j=1m[d|j])
那其实这后面两个就好算了,i=1n[d|i],这个式子对于确定的d本质就是在小于等于n的所有数字是d的倍数的数字的个数。这就等于nd
所以上式变为dmin(n,m)μ(d)ndmd,莫比乌斯函数可以快速的计算,后面两个东西可以通过数论分块来在O(sqrt(n)×sqrt(m))的时间内解决。总时间复杂度就是这个O(nm) (吧?) 。

就这题来看,主要运用的就是下面循环的整除。这个整除能够搞事情,现在看到的就是d|gcd(i,j),可以转化为[d|i][d|j],比较典型?(因为我之前想不到
还有非常重要的,虽然这属于公式推导的内容。就是循环顺序的调换规则,正常情况下是不会去动的,但是绝大多数时候如果是涉及式子的题目,特别是数学和多项式一类的,循环顺序是非常重要的,因为一般只有最后一个循环好进行操作,循环套循环是非常难套公式和变换的。
而这个法则我一般都是感性理解去操作的,像这里就是,调换过后式子变化挺大,但是这个调换顺序是最关键的一步。要能够意识到调换顺序后的是可以直接计算的。其实只要满足最后一个循环的底下的条件是d|gcd(i,j)或者等价形式就能够做到,当然μ(d)只能是和i,j无关的函数。

例二、给定两个数组a,b,求i=1nj=1mgcd(ai,bj)

还是,沿着上一个的思路,利用n=d|nϕ(d),创造n=d|gcd的结构,因为前面两个都是正常的循环,这样可以非常方便的帮助我们解决换顺序的问题。
这样就会变成i=1nj=1md|gcd(a[i],b[j])ϕ(d),然后调换顺序 dϕ(d)i=1nj=1m[d|ai][d|bj]
然后还是把[d|ai]放到前面,dϕ(d)(i=1n[d|ai])(j=1m[d|bj])
然后后面这两甚至不用整数分块。比上题还好写。

例三、给定两个数组a,b,求i=1nj=i+1n[gcd(ai,aj)=1]

ϵ(n)=d|nμ(d)替换后面的部分,得到i=1nj=i+1nd|gcd(ai,aj)μ(d)
然后和上面一样枚举dμ(d)提前,得到dnμ(d)i=1nj=i+1n[d|ai][d|aj],然后把[d|ai]提前,得到dnμ(d)i=1n[d|ai]j=i+1n[d|aj]
式子到这里就够了,直接对于每个d统计有多少个ai满足[d|ai]即可。然后就是组合数。

这些都是最基础的模型了。

例题们

[POI2007] ZAP-Queries

题目描述

密码学家正在尝试破解一种叫 BSA 的密码。

他发现,在破解一条消息的同时,他还需要回答这样一种问题:

给出 a,b,d,求满足 1xa1yb,且 gcd(x,y)=d 的二元组 (x,y) 的数量。

因为要解决的问题实在太多了,他便过来寻求你的帮助。

输入格式

输入第一行一个整数 n,代表要回答的问题个数。

接下来 n 行,每行三个整数 a,b,d

输出格式

对于每组询问,输出一个整数代表答案。

样例 #1

样例输入 #1

2
4 5 2
6 4 3

样例输出 #1

3
2

提示

数据规模与约定

对于全部的测试点,保证 1n5×1041da,b5×104

链接

其实就是求i=1aj=1b[gcd(i,j)=d],也就是i=1aj=1b[gcd(i,j)d=1]
再变形为i=1aj=1bD|gcd(i,j)dμ(D),把μ(D)提前,枚举DDmin(a,b)dμ(D)i=1aj=1b[D×d|gcd(i,j)]
Dmax(a,b)dμ(D)i=1a[D×d|i]j=1b[D×d|j],然后就是统计i=1a[D×d|i],其实就是aD×d

那么最终的式子就是Dmin(a,b)dμ(D)aD×dbD×d

单次就是O(a),总体O(n max(a,b)),会T,前面的部分直接筛出来,然后后面的部分要用整除分块优化,可以达到O(nmax(a,b))
然后就可以写了

#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline ll read(){
	ll a=0,b=1;char c=getchar();
	for(;c<'0'||c>'9';c=getchar())if(c=='-')b=-1;
	for(;c>='0'&&c<='9';c=getchar())a=a*10+c-'0';
	return a*b;
}const ll N=5e6;
ll phi[N+1],prim[N+1],cnt,mu[N+1];
unordered_map<ll,ll> ans_mu,ans_phi;
ll vis[N+1];
ll Sum_mu(ll n)
{
	if(n<=N)return mu[n];
	if(ans_mu[n])return ans_mu[n];
	ll ans=1;
	for(ll l=2,r;l<=n;l=r+1)
	{
		r=n/(n/l);
		ans-=1LL*(r-l+1)*(Sum_mu(n/l));
	}
	return ans_mu[n]=ans;
}
ll Sum_phi(ll n)
{
	if(n<=N)return phi[n];
	if(ans_phi[n])return ans_phi[n];
	ll ans=(n+1)*(n)/2;
	for(ll l=2,r;l<=n;l=r+1)
	{
		r=n/(n/l);
		ans-=(r-l+1)*Sum_phi(n/l);
	}
	return ans_phi[n]=ans;
}
void init()
{
	mu[1]=phi[1]=1;//先用筛法算出较小的部分
	for(ll i=2;i<=N;i++)
	{
		if(vis[i]==0)prim[++cnt]=i,mu[i]=-1,phi[i]=i-1;
		for(ll j=1;j<=cnt&&prim[j]*i<=N;j++)
		{
			vis[prim[j]*i]=1;
			if(i%prim[j]==0)
			{
				phi[i*prim[j]]=phi[i]*prim[j];
				mu[i*prim[j]]=0;
				break;
			}
			phi[i*prim[j]]=phi[i]*phi[prim[j]];
			mu[i*prim[j]]=mu[i]*mu[prim[j]];
		}
	}
	for(ll i=1;i<=N;i++)mu[i]+=mu[i-1],phi[i]+=phi[i-1];
}
int main()
{
    // freopen("1.in","r",stdin);
    // freopen("1.out","w",stdout);
    init();
    int T=read();
    while(T--)
    {
        int a=read(),b=read(),d=read();
        int ans=0;
        a=a/d,b=b/d;
        if(a<b)swap(a,b);
        for(int l=1,r=0;l<=(min(a,b));l=r+1)
        {
            // cout<<a/l<<' '<<b/l<<endl;
            r=min(a/(a/l),b/(b/l));
            ans+=(Sum_mu(r)-Sum_mu(l-1))*(a/l)*(b/l);
        }
        cout<<ans<<endl;
    }
    return 0;
}

主要学一下两个函数同时限制的整除分块。

YY的GCD

题目描述

神犇 YY 虐完数论后给傻× kAc 出了一题

给定 N,M,求 1xN1yMgcd(x,y) 为质数的 (x,y) 有多少对。

输入格式

第一行一个整数 T 表述数据组数。

接下来 T 行,每行两个正整数,N,M

输出格式

T 行,每行一个整数表示第 i 组数据的结果。

样例 #1

样例输入 #1
2
10 10
100 100
样例输出 #1
30
2791

提示

T=104N,M107

链接

先简单转化一下,这个形式还是没法直接反演的。
要求为质数,其实就是σ(i)=i+1,所以直接用这个表示i=1Nj=1M[σ(gcd(i,j))=gcd(i,j)+1]
这个嵌套...拆不出来吧...
思路错了,之前都是枚举Gcd的值,这里也直接枚举就没问题了。。。这个就是最典型的反演套路

kprimemax(N,M)i=1N/kj=1M/k[gcd(i,j)=1]

kprimemax(N,M)i=1N/kj=1M/kd|gcd(i,j)μ(d)

kprimemax(N,M)dNkμ(d)i=1N/kj=1M/k[d|i][d|j]

那就是kprimemax(N,M)dNkμ(d)NkdMkd
发现一遍枚举质数k一遍整除分块会爆炸,因为N,M1e7的数量级。
先枚举质数显然不可取,质数的数量很大。
考虑先进行整除分块,那么需要调换枚举顺序。
T=1NNTMTk|T,kprimeμ(Tk)

其实上面的这一步有点复杂,但是还是能够勉强理解的。这样就可以计算了,因为后面的部分可以发现和NM没有任何关系,直接预处理然后O(1)查询即可,前面的部分整除分块搞定。
这题的主要思路也是转化,但是最后一步的优化思路是比较麻烦的。虽然在原本的第一重循环内没有含有d的变量,但是调换顺序之后是否能够预处理还是取决于后面的循环的结构,甚至其实是取决于后面的循环的计算内容。这个里面就是,最后循环中k的结构就是和先前的NkdMkd底下的分母相关。不过其实有一个非常明显的思路,就是整除分块放前面是没什么问题的,多重循环时,整除循环的位置很多时候决定了复杂度。而且同时整除循环对于循环有一定要求,尽量让整除分块只做一次就好了。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline ll read()
{
    ll a=0,b=1;char c=getchar();
    for(;c<'0'||c>'9';c=getchar())if(c=='-')b=-1;
    for(;c>='0'&&c<='9';c=getchar())a=a*10+c-'0';
    return a*b;
}
ll mu[10000010],flag[10000010],prime[10000010],cnt,f[10000010],sum[10000010];
int main()
{
    mu[1]=1;
    for(ll i=2;i<=10000000;i++)
    {
        if(flag[i]==0)prime[++cnt]=i,mu[i]=-1;
        for(ll j=1;j<=cnt&&i*prime[j]<=10000000;j++)
        {
            flag[i*prime[j]]=1;
            if(i%prime[j]==0)break;
            mu[i*prime[j]]=mu[i]*mu[prime[j]];
        }
    }
    for(ll i=1;i<=cnt;i++)
    {
        for(ll j=1;prime[i]*j<=10000000;j++)
        {
            f[j*prime[i]]+=mu[j];
        }
    }
    for(ll i=1;i<=10000000;i++)
    {
        sum[i]=sum[i-1]+f[i];
    }
    ll T=read();
    while(T--)
    {
        ll l=1,r=0,ans=0;
        ll n=read(),m=read();
        if(n>m)swap(n,m);
        for(;l<=n;l=r+1)
        {
            r=min(n/(n/l),m/(m/l));
            ans+=1LL*(n/l)*(m/l)*(sum[r]-sum[l-1]);
        }
        cout<<ans<<endl;
    }
    return 0;
}

[HAOI2011] Problem b

题目描述

对于给出的 n 个询问,每次求有多少个数对 (x,y),满足 axbcyd,且 gcd(x,y)=kgcd(x,y) 函数为 xy 的最大公约数。

输入格式

第一行一个整数 n,接下来 n 行每行五个整数,分别表示 a,b,c,d,k

输出格式

n 行,每行一个整数表示满足要求的数对 (x,y) 的个数。

样例 #1

样例输入 #1

2
2 5 1 5 1
1 5 1 5 2

样例输出 #1

14
3

提示

对于 100% 的数据满足:1n,k5×1041ab5×1041cd5×104

其实可以拆分成4个询问然后合并成答案,这样整除分块好算。
我们只要能够在合理的时间内求出i=1nj=1m[gcd=k]就好了。而这个是板子。哦,这个是往上数的前两题。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline int read()
{
    char c=getchar();int a=0,b=1;
    for(;c<'0'||c>'9';c=getchar())if(c=='-')b=-1;
    for(;c>='0'&&c<='9';c=getchar())a=a*10+c-'0';
    return a*b;
}
const int N=5e4;
int mu[N+1],prime[N+1],cnt,vis[N+1];
inline int count(int a,int b)
{
    int ans=0;
    for(int l=1,r=0;l<=min(a,b);l=r+1)
    {
        r=min(a/(a/l),b/(b/l));
        ans+=(a/l)*(b/l)*(mu[r]-mu[l-1]);
    }
    return ans;
}
int main()
{
    mu[1]=1;
    for(int i=2;i<=N;i++)
    {
        if(vis[i]==0)prime[++cnt]=i,mu[i]=-1;
        for(int j=1;j<=cnt&&i*prime[j]<=N;j++)
        {
            vis[i*prime[j]]=1;
            if(i%prime[j]==0)
            {
                mu[i*prime[j]]=0;
                break;
            }
            mu[i*prime[j]]=mu[i]*mu[prime[j]];
        }
    }
    // for(int i=1;i<=100;i++)
    // {
    //     cout<<mu[i]<<' ';
    // }
    // cout<<endl;
    for(int i=1;i<=N;i++)mu[i]=mu[i-1]+mu[i];
    int T=read();
    while(T--)
    {
        int a=read(),c=read(),b=read(),d=read(),k=read();
        cout<<count(c/k,d/k)-count(c/k,(b-1)/k)-count((a-1)/k,d/k)+count((a-1)/k,(b-1)/k)<<endl;
    }
    return 0;
}
posted @   HL_ZZP  阅读(14)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示