数论总结一(数论函数)
写在前面
这是一个让我又爱又恨的版块,每一次我学了可能就会,但是过不了多久就又不会了。所以每次重新遇到又要重新学一遍,平时只记得名字,特别菜。希望这次写完可以有一点提升。
数论函数
基本概念
数论函数:定义域为正整数的函数
加性函数:对于所有 \(\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应用较广):它是积性函数
狄利克雷卷积
百度认证的数论函数最重要的运算。
定义
两个数论函数\(f,g\)的狄利克雷卷积为 \((f*g)(n)=\sum_{d|n}f(d)g(n/d)=\sum_{d|n}f(d)g(n/d)\)
性质
显然有交换律,结合律,分配律。(按定义展开即可)。
2.单位元(运算中的1)是 \(\omega\)。
证明
3.积性函数狄利克雷逆(有单位元就可以定义逆元了)仍是积性函数。
4.两个积性函数的狄利克雷卷积仍是积性函数。
(3,4的证明等有空再补)
数论函数之间的关系
- \(\mu*1=\omega\)
证明:
数论分块
在开始讲莫反前,需要先了解一个工具数论分块。
数论分块可以快速计算一些含有除法向下取整的和式(即形如 \(\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\) 的性质。
证明在上面。
然后我们需要知道一个事实,定义两个数论函数\(g(x)\) 和 \(f(x)\) 如果满足 \(g(x)=\sum_{d|n} f(d)\) 则有 \(f(n)=\sum_{d|n}\mu(d)g(n/d)\) 。
总结一下。
证明(反演的证明一般只需要代入即可):
例题一
形式化的题意就是求
对于这种gcd相关的一个和式就可以考虑试一试莫反。
化到这就差不多了,但是单次的时间复杂度是 \(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\)。
我们可以快速调到上一题的最后一步变成求
但是直接暴力还是过不去。所以我们考虑接着交换和式,改变变量。
我们发现前面是根号的,后面可以 \(\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)\) 进行之间的转化。
我们发现后面\(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] 简单的数论题
有点实力的题。
化到这一步,我们发现后面一坨东西非常相似都形如 \(\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;
}

浙公网安备 33010602011771号