数论总结一(数论函数)

写在前面

这是一个让我又爱又恨的版块,每一次我学了可能就会,但是过不了多久就又不会了。所以每次重新遇到又要重新学一遍,平时只记得名字,特别菜。希望这次写完可以有一点提升。

数论函数

基本概念

数论函数:定义域为正整数的函数
加性函数:对于所有 \(\forall n,m\in N ^∗,\gcd(n,m)=1\),满足 \(f(n)+f(m)=f(nm)\)
完全加性函数:对于所有\(\forall n,m\in N ^∗\),满足 \(f(n)+f(m)=f(nm)\)
(加性函数的证明,先证 \(f(1)=0\),再证在互质时两边相等)
积性函数: 对于所有 \(\forall n,m\in N ^∗,\gcd(n,m)=1\),满足 \(f(n)f(m)=f(nm)\)
完全积性函数:对于所有\(\forall n,m\in N ^∗\),满足 \(f(n)f(m)=f(nm)\)
(加积性函数的证明,先证 \(f(1)=1\),再证在互质时两边相等)
一个blog推荐;这里有更多

常见的函数

单位函数:\(\varepsilon(n)=[n=1]\) 完全积性函数。
常量函数: \(1(n)=1\) 完全积性函数。
幂函数:\(id_k(n)=n^k\) 完全积性函数。
除数函数: \(\sigma _k(n)=\sum_{d|n}d^k\) 积性函数。
本质不同的质因子个数函数:\(\omega (n)=\sum_{i=1}^n [gcd(i,n)]=1\),它表示一个数本质不同的质因子个数 (有点废话),他是加性函数
莫比乌斯函数(在oi应用较广):它是积性函数

\[\mu(n)= \begin{cases} 1,n=1\\ 0,\exists d>1 d^2|n\\ -1^{\omega(n)},otherwise \end{cases} \]

狄利克雷卷积

百度认证的数论函数最重要的运算。

定义

两个数论函数\(f,g\)的狄利克雷卷积为 \((f*g)(n)=\sum_{d|n}f(d)g(n/d)=\sum_{d|n}f(d)g(n/d)\)

性质

显然有交换律,结合律,分配律。(按定义展开即可)。
2.单位元(运算中的1)是 \(\omega\)

\[f*\omega=\omega*f=f \]

证明

\[\omega*f=\sum_{d|n}[d=1]f(n/d)=f(n) \]

3.积性函数狄利克雷逆(有单位元就可以定义逆元了)仍是积性函数。
4.两个积性函数的狄利克雷卷积仍是积性函数。
(3,4的证明等有空再补)

数论函数之间的关系

  1. \(\mu*1=\omega\)
    证明:

\[\begin{aligned} (\mu*1)(n) &= \sum_{d|n}\mu(d) \\ &=sum_{i=0}^{\omega(n)}\binom{\omega(n)}{i}(-1)^i\\ &= [-1+1]^{\omega(n)}\\ &=[\omega(n)=0]\\ &=[n=1]\\ &=\varepsilon(n) \end{aligned} \]

数论分块

在开始讲莫反前,需要先了解一个工具数论分块。
数论分块可以快速计算一些含有除法向下取整的和式(即形如 \(\sum_{i=1}^nf(i)g(\left\lfloor\dfrac ni\right\rfloor)\) 的和式)。当可以在 \(O(1)\) 内计算 \(f(r)-f(l)\) 或已经预处理出 \(f\) 的前缀和时,数论分块就可以在 \(O(\sqrt n)\) 的时间内计算上述和式的值。
思路非常简单,我们发现 \(\left\lfloor\dfrac ni\right\rfloor\) 最多有 \(2\sqrt{n}\) 种取值(简要证明,\(i<=\sqrt{n}\)\(\left\lfloor\dfrac ni\right\rfloor\)最多有\(\sqrt{n}\)种取值,\(i\ge\sqrt{n}\)时,\(\left\lfloor\dfrac ni\right\rfloor\le\sqrt{n}\),所以最多\(\sqrt{n}\)种取值。
所以将取值相同的 \(i\) 统一计算。
这是时候需要一个方法快速求出取值相同的区间,这有一个结论是如果已知区间的左端点\(l\),则右端点 \(r=\left \lfloor {n\over {\left \lfloor n\over l \right \rfloor}} \right \rfloor\)
所以我们就可以有下面的代码

for(int l=1,r;l<=n;l=r+1)
		{
			r=n/(n/l);
			ans+=...; 
		} 

证明及更多

莫比乌斯反演

首先,我们需要一个 \(\mu\) 的性质。

\[\sum_{d|n}\mu(d)=[n=1] \]

证明在上面。
然后我们需要知道一个事实,定义两个数论函数\(g(x)\)\(f(x)\) 如果满足 \(g(x)=\sum_{d|n} f(d)\) 则有 \(f(n)=\sum_{d|n}\mu(d)g(n/d)\)
总结一下。

\[ g(x)=\sum_{d|n}f(d)\Leftrightarrow f(n)=\sum_{d|n}\mu(d)g(n/d) \]

证明(反演的证明一般只需要代入即可):

\[\begin{aligned} f(n)&=\sum_{d|n}\mu(n/d)g(d)\\ &=\sum_{d|n}\mu(n/d)\sum_{k|d}f(k)交换和式\\ &=\sum_{k|n}\sum_{d|{n\over k}}\mu({{n \over k}\over d})f(k)\\ &=\sum_{k|n}f(k) \end{aligned} \]

例题一

形式化的题意就是求

\[\sum_{i=1}^{a}\sum_{j=1}^{b}[gcd(i,j)=d] \]

对于这种gcd相关的一个和式就可以考虑试一试莫反。

\[\begin{aligned} \sum_{i=1}^{a}\sum_{j=1}^{b}[gcd(i,j)=d]&=\sum_{i=1}^{\left \lfloor {a\over d} \right \rfloor}\sum_{j=1}^{\left \lfloor {b\over d} \right \rfloor}[gcd(i,j)=1]\\ &=\sum_{i=1}^{\left \lfloor {a\over d} \right \rfloor}\sum_{j=1}^{\left \lfloor {b\over d} \right \rfloor}\sum_{k|gcd(i,j)}\mu(k) 根据\mu的性质\\ &=\sum_{k=1}^{min(\left \lfloor {a\over d} \right \rfloor,\left \lfloor {b\over d} \right \rfloor)}\mu(k)\sum_{i=1}^{\left \lfloor {a\over kd} \right \rfloor}\sum_{j=1}^{\left \lfloor {b\over kd} \right \rfloor}1 交换和式\\ &=\sum_{k=1}^{min(\left \lfloor {a\over d} \right \rfloor,\left \lfloor {b\over d} \right \rfloor)}\mu(k)\left \lfloor {a\over kd} \right \rfloor\left \lfloor {b\over kd} \right \rfloor \end{aligned} \]

化到这就差不多了,但是单次的时间复杂度是 \(O(5e4)\) 的,所以总的时间复杂度是 \(O(2.5e9)\) 就爆炸了。
再用整数分块即可。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N=5e4+10;
int prim[N],cnt,mu[N],sum[N];
bool vis[N];
void init()
{
	mu[1]=1;
	for(int i=2;i<=N-10;i++)
	{
		if(!vis[i])prim[++cnt]=i,mu[prim[cnt]]=-1;
		for(int j=1;j<=cnt&&i*prim[j]<=N-10;j++)
		{
			vis[i*prim[j]]=1;
			if(i%prim[j]==0)break;	
			mu[i*prim[j]]=mu[i]*mu[prim[j]];
		}
	}
	for(int i=1;i<=N-10;i++) sum[i]=sum[i-1]+mu[i];	
}
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	init();
	int t;cin>>t;
	while(t--)
	{
		int a,b,d;cin>>a>>b>>d;
		long long ans=0; 
		a/=d,b/=d;
		if(a>b)swap(a,b);//保证a<b;
		for(int st=1,ed;st<=a;st=ed+1)
		{
			ed=min(a/(a/st),b/(b/st));
			ans+=1ll*(sum[ed]-sum[st-1])*(a/st)*(b/st); 
		} 
		cout<<ans<<'\n';
	}
} 

P2257 YY的GCD

我们发现这个题与上个题非常的像,唯一不同的是需要枚举质数 \(k\)
我们可以快速调到上一题的最后一步变成求

\[\begin{aligned} \sum_{k=1,k是质数}^{n}\sum_{d=1}^{\left \lfloor {n\over k} \right \rfloor}\mu(d)\left \lfloor {a\over kd} \right \rfloor\left \lfloor {b\over kd} \right \rfloor \end{aligned} \]

但是直接暴力还是过不去。所以我们考虑接着交换和式,改变变量。

\[\begin{aligned} \sum_{T=1}^{n}\left \lfloor {a\over T} \right \rfloor\left \lfloor {b\over T} \right \rfloor \sum_{k是质数}^{T}\mu({T\over k}) \end{aligned} \]

我们发现前面是根号的,后面可以 \(\log^2\) 预处理(像埃氏筛一样)。总时间就可以过了。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N=1e7;
int t,n,m,mo[N+10],prim[N+10],f[N+10],p;
bool vis[N+10];
void ycl()
{
	mo[1]=1;
	for(int i=2;i<=N;i++)
	{
		if(!vis[i])
		{
			mo[i]=-1;
			prim[++p]=i; 
		}
		for(int j=1;j<=p&&prim[j]*i<=N;j++)
		{
			vis[i*prim[j]]=1;
			if(i%prim[j]==0)break;
			mo[i*prim[j]]=-mo[i];
		}
	}
	for(int i=1;i<=p;i++)
	{
		for(int j=1;j*prim[i]<=N;j++)
			f[j*prim[i]]+=mo[j];
	}
	for(int i=1;i<=N;i++)f[i]+=f[i-1]; 
}
long long js(int n,int m)
{
	long long ans=0;
	if(n>m)swap(n,m);
	for(int l=1,r;l<=n;l=r+1)
	{
		r=min(n/(n/l),m/(m/l));
		ans+=1ll*(f[r]-f[l-1])*1ll*(n/l)*1ll*(m/l);
	}
	return ans;
}
int main()
{
	ycl();
	scanf("%d",&t);
	while(t--)
	{
		scanf("%d%d",&n,&m);
		if(n>m)swap(n,m); 
		printf("%lld\n",js(n,m));
	}		
	return 0;
}

P2522 [HAOI2011] Problem b

与第一道例题相差无几,唯一的区别是下界不是1,而是一个给出的值,所以可以考虑差分一下后面变成 \((\left \lfloor {b\over t} \right \rfloor-\left \lfloor {(a-1)\over t} \right \rfloor)(\left \lfloor {d\over t} \right \rfloor-\left \lfloor {(c-1)\over t} \right \rfloor)\)。然后括号拆开,四部分分别算即可。注意-1的位置(我最开始就错误的把它放下来了)

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N=5e4+10;
int prim[N],cnt,mu[N],sum[N];
bool vis[N];
void init()
{
	mu[1]=1;
	for(int i=2;i<=N-10;i++)
	{
		if(!vis[i])prim[++cnt]=i,mu[prim[cnt]]=-1;
		for(int j=1;j<=cnt&&i*prim[j]<=N-10;j++)
		{
			vis[i*prim[j]]=1;
			if(i%prim[j]==0)break;	
			mu[i*prim[j]]=mu[i]*mu[prim[j]];
		}
	}
	for(int i=1;i<=N-10;i++) sum[i]=sum[i-1]+mu[i];	
}
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	init();
	int t;cin>>t;
	while(t--)
	{
		int a,b,c,d,k;cin>>a>>b>>c>>d>>k;
		long long ans=0; 
		a=(a-1)/k,b/=k,c=(c-1)/k,d/=k;
		for(int st=1,ed;st<=min(b,d);st=ed+1)
		{
			ed=min(b/(b/st),d/(d/st));
			ans+=1ll*(sum[ed]-sum[st-1])*(b/st)*(d/st); 
		} 
		for(int st=1,ed;st<=min(a,c);st=ed+1)
		{
			ed=min(a/(a/st),c/(c/st));
			ans+=1ll*(sum[ed]-sum[st-1])*(a/st)*(c/st); 
		} 
		for(int st=1,ed;st<=min(a,d);st=ed+1)
		{
			ed=min(a/(a/st),d/(d/st));
			ans-=1ll*(sum[ed]-sum[st-1])*(a/st)*(d/st); 
		}
		for(int st=1,ed;st<=min(b,c);st=ed+1)
		{
			ed=min(b/(b/st),c/(c/st));
			ans-=1ll*(sum[ed]-sum[st-1])*(b/st)*(c/st); 
		} 
		cout<<ans<<'\n';
	}
} 

P1829 [国家集训队] Crash的数字表格 / JZPTAB

这题也启示我们不仅是 \(gcd\) 通过 \(ij=gcd(i,j)*lcm(i,j)\) 进行之间的转化。

\[\begin{aligned} \sum_{i=1}^{n}\sum_{j=1}^{m}lcm(i,j) &=\sum_{i=1}^{n}\sum_{j=1}^m {ij\over gcd(i,j)}\\ &=\sum_{d=1}^n\sum_{i=1}^{n}\sum_{j=1}^m[gcd(i,j)=d]{ij\over d}枚举gcd\\ &=\sum_{d=1}^nd\sum_{i=1}^{{n\over d}}\sum_{j=1}^{m\over d}[gcd(i,j)=1]ij注意除d后ij的位置要乘d乘回去\\ &=\sum_{d=1}^nd\sum_{k=1}^{{n\over d}}\mu(k)k^2\sum_{i=1}^{{n\over dk}}\sum_{j=1}^{m\over dk}ij经典套路\\ \end{aligned} \]

我们发现后面\(i,j\)的部分可以等差数列 \(O(1)\) 求出来。前面 \(k\) 可以前缀和预处理。然后 \(d\)数论分块即可。总时间复杂度为 \(O(n)\)

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N=1e7+10,mod=20101009;
#define int long long
int prim[1000010],cnt,mu[N],sum[N],inv2,ans;
bool vis[N];
void init(int n)
{
	mu[1]=1;
	for(int i=2;i<=n;i++)
	{
		if(!vis[i])prim[++cnt]=i,mu[prim[cnt]]=-1;
		for(int j=1;j<=cnt&&i*prim[j]<=n;j++)
		{
			vis[i*prim[j]]=1;
			if(i%prim[j]==0)break;	
			mu[i*prim[j]]=mu[i]*mu[prim[j]]%mod;
		}
	}
	for(int i=1;i<=n;i++) sum[i]=(sum[i-1]+mu[i]*i%mod*i%mod)%mod;	
}
int G(int x,int y) {return 1ll*x*(x+1)%mod*y%mod*(y+1)%mod*inv2%mod*inv2%mod;}
int Sum(int n,int m) 
{
	int res=0;
	for(int st=1,ed;st<=n;st=ed+1)
	{
		ed=min(n/(n/st),m/(m/st));
		res=(res+(sum[ed]-sum[st-1]+mod)%mod*G(n/st,m/st)%mod)%mod;
	}
	return res;
}  
int ksm(int x,int y)
{
	int res=1;
	while(y)
	{
		if(y&1)res=res*x%mod;
		x=x*x%mod;
		y>>=1; 
	} 
	return res;
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	
	int n,m;cin>>n>>m;
	if(n>m)swap(n,m);
	init(n);
	inv2=ksm(2,mod-2);
	for(int st=1,ed;st<=n;st=ed+1)
	{
		ed=min(n/(n/st),m/(m/st));
		int len=(ed-st+1);
		ans=(ans+len*(st+ed)%mod*inv2%mod*Sum(n/st,m/st)%mod)%mod;
	}
	cout<<ans<<'\n';
} 

P5572 [CmdOI2019] 简单的数论题

有点实力的题。

\[\begin{aligned} &\sum_{i=1}^n\sum_{j=1}^m\varphi({lcm(i,j)\over \gcd(i,j)})\\ &\sum_{i=1}^n\sum_{j=1}^m\varphi({ij\over \gcd^2(i,j)})\\ &\sum_{d=1}^n\sum_{i=1}^{n/d}\sum_{j=1}^{m/d}\varphi(ij)[gcd(i,j)=1]枚举gcd\\ &\sum_{d=1}^n\sum_{i=1}^{n/d}\sum_{j=1}^{m/d}\varphi(i)\varphi(j)\sum_{k|gcd(i,j)}\mu(k)经典再现+积性函数\\ &\sum_{d=1}^n\sum_{k=1}^{n/d}\mu(k)\sum_{i=1}^{n/kd}\varphi(ik)\sum_{j=1}^{m/kd}\varphi(jk)\\ &\sum_{T=1}^n\sum_{d|T}\mu(d)\sum_{i=1}^{n/T}\varphi(ik)\sum_{j=1}^{m/T}\varphi(jk) \end{aligned} \]

化到这一步,我们发现后面一坨东西非常相似都形如 \(\sum_{i=1}^{x}\varphi(ik)\) 所以我们设一个新的函数 \(g(k,x)\) 表示 \(\sum_{i=1}^{x}\varphi(ik)\) 枚举 \(k,x\) 计算来预处理的时间复杂度为 \(O(n\log n)\) 调和级数。原式变为 \(\sum_{T=1}\sum_{d|T}\mu(d)g(n/kd,k)g(m/kd,k))\) ,直接暴力的时间复杂度为 \(O(Tn\log n)\) 显然不行。

我们发现整数分块还没有用,这也是为什么时间复杂度很不优。

这时候我们再用(可能是常见套路) \(h(x,y,z)=\sum_{i=1}^z\sum_{d|i}\mu(d)g(d,x)g(d,y)\)

现在我们发现对于 \(\left \lfloor n/i \right \rfloor\)\(\left \lfloor m/i \right \rfloor\) 相同的段我们对于答案的贡献就是 \(h(\left \lfloor n/i \right \rfloor,\left \lfloor m/i \right \rfloor,r)-h(\left \lfloor n/i \right \rfloor,\left \lfloor m/i \right \rfloor,l-1)\) 我们发现这样就可以整数分块了,但是新的问题产生了,如果我们可以预处理出所有 \(h\) 的话就能做完了,但是直接预处理的时间复杂度是 \(O(nm^2\log m)\) 显然不可接受。

这个时候我们发现 \(h(x,y,z)\)\(x,y\) 较小的时候, 调用的次数多但是计算时间小。相反较大的时候,次数比较少,但是计算时间大。所以我们可以考虑根号分治来平衡时间复杂度。

具体来说就是设置一个 \(B\) ,对于 \(\left \lfloor m/B \right \rfloor\)\(z\) 都暴力计算,时间复杂度是 \(O({m\over B}\log {m\over B})\) 对于大于的部分预处理,这时预处理的时间复杂度为 \(O(B^2 m\log m)\) 通过计算我们(观察别人的题解)发现 \(B\)\(20\)\(100\) 比较好 。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N=5e4+5,mod=23333,B=50;
int n,m,mu[N],phi[N],prim[N],tot,vis[N];
vector<int> G[B+10][B+10],yz[N],gs[N];
int calc(int x,int y,int z)
{
	int res=0;
	int len=yz[z].size();
	for(int j=0;j<len;j++)
	{
		int k=yz[z][j];
		res=(res+mu[k]*gs[k][x]%mod*gs[k][y]%mod+mod)%mod; 
	}
	return res;
}
void init()
{
	mu[1]=phi[1]=1;
	for(int i=2;i<N;i++)
	{
		if(!vis[i])prim[++tot]=i,mu[i]=-1,phi[i]=i-1;
		for(int j=1;j<=tot&&prim[j]*i<N;j++)
		{
			vis[prim[j]*i]=1;
			if(i%prim[j]==0)
			{
				phi[i*prim[j]]=prim[j]*phi[i];
				break;
			}
			phi[i*prim[j]]=phi[i]*phi[prim[j]];
			mu[i*prim[j]]=mu[i]*mu[prim[j]];
		}
	}
	for(int i=1;i<N;i++)
		for(int j=i;j<N;j+=i)
			yz[j].push_back(i);
	for(int i=1;i<N;i++)
	{
		gs[i].push_back(0);
		for(int j=1;j*i<N;j++)
		{
			gs[i].push_back((gs[i][j-1]+phi[j*i])%mod); 
		}		
	}		
	for(int i=1;i<=B;i++)
		for(int j=1;j<=B;j++)
		{
			G[i][j].push_back(0);//占位置 
			for(int z=1;z*max(i,j)<N;z++)
			{
				G[i][j].push_back((G[i][j][z-1]+calc(i,j,z)+mod)%mod);
				//cout<<G[i][j][z]<<'\n';
			}
				
		}
}
void solve()
{
	cin>>n>>m;swap(n,m);
	int ans=0;
	for(int i=1;i*B<=m;i++)ans=(ans+calc(n/i,m/i,i))%mod;
	for(int l=m/B+1,r;l<=n;l=r+1)
	{
		r=min(n/(n/l),m/(m/l));
		ans=(ans+G[n/l][m/l][r]-G[n/l][m/l][l-1]+mod)%mod;
	}
	cout<<ans<<'\n';
}
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	int t;cin>>t;
	init();
	while(t--)solve();
	return 0;
}
posted @ 2025-03-17 21:20  exCat  阅读(114)  评论(0)    收藏  举报