简单的数论函数及其求和方法
数论函数#
记号与约定#
称函数 为数论函数,当且仅当其定义域为 ,陪域为 。
称数论函数 为积性函数当且仅当对于任意 ,有 。
称数论函数 为完全积性函数当且仅当对于任意 ,有 。
按照如下记号记录部分数论函数:
- 定义照常,不再赘述。
其中 完全积性。
另外,文中部分 型分数根据上下文可能有向下取整()的含义,请注意甄别。
线性筛#
对于任意积性函数,我们可以线性筛求出。
我们只需要关心积性函数在素数幂 处的取值。为了检验一个数 是否是 型的,我们可以维护 表示 中最小质因子的部分。例如 。当且仅当 时 是 型的。
具体的 和 也是不难维护的。
下面的模板来自 Alex-Wei。
for(int i = 2; i < N; i++) {
if(!vis[i]) pr[++pcnt] = i, f[i] = ..., low[i] = i; // 单独算 f(p)
for(int j = 1; j <= pcnt && i * pr[j] < N; j++) {
vis[i * pr[j]] = 1;
if(i % pr[j] == 0) { // i 与 p 不互质
low[i * pr[j]] = low[i] * pr[j];
if(i == low[i]) f[i * pr[j]] = ...; // i = p ^ k,单独算 f(p ^ {k + 1})
else f[i * pr[j]] = f[i / low[i]] * f[low[i * pr[j]]];
break;
}
low[i * pr[j]] = pr[j];
f[i * pr[j]] = f[i] * f[pr[j]]; // i 与 p 互质,f(ip) = f(i)f(p)
}
}
两例变量分离#
-
构造双射容易证明。
-
证明:
关于 #
有性质:
当 时,必然不存在平方因子,因此恰有 造成 的贡献。
当 时,设 这些质因子在 中的次数至少为 。 要在右侧造成贡献,只能是这些质因子构成的集合的某个子集的乘积,根据二项式定理,贡献为 。
狄利克雷卷积#
狄利克雷卷积#
狄利克雷卷积:是一个算子。对于数论函数 ,我们有
狄利克雷逆:若 则称 互为狄利克雷逆。
对于 的 , 的狄利克雷逆一定存在。显然这条件已经必要。如下推导得到 :
基本性质#
-
交换律
-
结合律
-
分配律
-
消去律
-
两个积性函数的狄利克雷卷积是积性函数
-
积性函数的狄利克雷逆是积性函数
几个简单的狄利克雷卷积#
-
这也是为什么我们可以把 化开变成 。
-
这也是为什么我们可以把 化开变成 。
-
根据 的定义。
-
根据 的定义。
狄利克雷卷积的求法 (特别鸣谢: pp_orange)#
已知,每一项可以 求出。设我们要计算 的前 项。
当 是普通函数时#
采用定义式 计算。
Code:[V 我 50 我帮你写]。
当 为积性函数时#
考察 这一特例。此时等价于狄利克雷前缀和,复杂度为 。
当 为任意积性函数时,我们可以执行类似的操作。考虑枚举质数 。初始我们的卷积中只有 这一项。每枚举一个质数 ,我们就枚举不含因子 的正整数 ,把 这样的项贡献到对应的新的 上。这样我们就相当于在 这个维度做了一遍前缀和,现在 里面包含了所有 的项,其中 整除 ,且 的质因子集合中只有我们枚举过的质数。
因为高维前缀和是对的,那么这个操作也应当是对的。此时复杂度为 。这是因为对于每个 ,后面的部分都可以看作余项(等比数列求和不超过常数倍的第一项),因此其复杂度和狄利克雷前缀和 / 埃氏筛法相同。
Code:[V 我 50 我帮你写]。
当 为积性函数时#
时间复杂度为 。
我们只需要求出 在素数幂处的取值。对于每个素数直接暴力,小于等于 的素数复杂度可以看成 ,而对于大于 的素数只会有一项。因此,计算素数幂处取值的复杂度可以看作低阶项,不影响用 计算的复杂度。
Code:[V 我 50 我帮你写]。
杜教筛#
前置: 公式#
- :。
- :。
- :。
- :。已经很少用到了。
- 对于 ,使用连续点值拉格朗日插值可以 计算一次。
流程#
考虑求某数论函数 的前缀和 。杜教筛的想法是通过一个比较好的数论函数 和比较好的 把 的前缀和化简。这和 是否是积性函数无关。
我们知道
如果 和 的前缀和比较简单,我们就可以快速求出 的前缀和了。
考虑如下伪代码:
inline int SumF(int n){ // 注意:这里计算的是 g(1)S(n). 但大部分时候 g 是积性函数或其点乘,因此 g(1)=1.
int ans=SumFG(n);
for(int l=2,r;l<=n;l=r+1){
r=n/(n/l);
ans-=(SumG(r)-SumG(L-1))*SumF(n/l);
}
return ans;
}
当使用记忆化的时候,根据整除分块经典结论,我们只会用到不超过 个值。
那么 。求和改积分容易证得 。
否则时间复杂度不详(OI-Wiki 对不加以记忆化的时间复杂度证明作出了证伪:不能直接将第二层之后的 看作余项)。
如果我们预处理部分 ,设预处理复杂度为 ,则总复杂度为
那么一般 ,此时 最优。
详见 OI-Wiki。
两例简单的杜教筛#
-
根据 ,取 。
-
根据 ,取 。
点乘#
定义 点乘的结果 是一个函数,满足 。
对于数论函数 ,当 是完全积性函数的时候,我们有
那么当我们要求前缀和的函数 为两个简单函数点乘的时候,我们可以运用这个等式,从而寻求形式较好的 和 ,然后杜教筛解决。
下面是两例比较基本的点乘处理方法。
-
:取 。
-
:取 。
下面是抄的一例点乘:
- :取 。而 的前缀和可以通过公式 计算:简单地交换求和号,前缀和变为 。直接计算即可做到 ,求出 的前缀和可以进一步做到 。但是在低于 时间复杂度内求出 的前缀和需要再写筛法,其实就不如直接 求了。
另一例略显奔放的点乘(出自某场联考的最后一部分):
- :取 。红色部分是第一例点乘中出现的结果,它等于 。
小结:欲求 形的前缀和,我们希望 形如 ,且 的前缀和易求。然后,运用上面的等式将 化作 。适当地选取 可以将 化作简单的函数,从而 的前缀和易求。
简单运用#
例 1#
求
对某个大质数取模
经过平凡的推导可以得出原式等于
想要整除分块,则需要求出 的前缀和。
令 。利用杜教筛求出 和 的前缀和即可。注意到复杂度仍然是 的,因为我们可以线性筛预处理 的前 项。
例 2 (Luogu P3768 简单的数学题)#
求
对某个大质数取模
经过平凡的推导可以得出原式等于
我们知道了怎么处理点乘,因此杜教筛也是容易的:。
例 3#
求
这碟醋终于出现了!
平凡推导可得原式等于
按照上面点乘处的推导,令 ,请出 ,。
一个粗劣的复杂度上界是 ,但事实上整个杜教筛过程中也只会用到 个 的值,对它们记忆化可以分析出更好的复杂度 。
[NOI2016] 循环之美#
简要题面:
这题处理技巧比较奇怪,我们不太能直接把两个艾弗森括号都拆成 ,而是先保留下来 的那部分。在这里,和一个给定常数互质这个条件是比较好的。
然后要求两个东西:
对于 ,根据 ,艾弗森括号的取值是 - 循环的,我们只需要预处理 即可。
对于 则比较需要有想象力。
递归计算,直到递归到 时用杜教筛求 前缀和。
注意到最终要算 个 , 这一维是 的因子,因此总复杂度为 。
BZOJ3512: DZY Loves Math IV#
求
根据《毒瘤之神的考验》一题快进到
记 ,则上式可写作
在《毒瘤之神的考验》一题中,要用到的全体 可 预处理,从而最终复杂度也是 的。
本题需要神秘的观察。我们考虑类似扰动法的策略,
注意到 这一维是小于等于给定的 的(和此处的变量 作区分),于是对于 这样的状态来说,计算它们的复杂度不太大。我们可以采用记忆化 + 预处理小的部分来减小递归的复杂度。
Min_25 筛#
综述#
考虑求一类积性函数的前缀和,其中 是积性函数,满足:
- 对于任意素数 , 处取值是关于 的低次多项式。
- 处取值容易求得。
则 Min_25 筛可以在 或 的复杂度内求出 的前缀和。
流程#
Min_25 筛的求解分为两步:
- 对于 ,考虑其中每一个元素 ,求出 ,即前缀素数点处取值之和;
- 根据第一步求得的 个答案进而求解出整个前缀和。
第一步#
因为 的取值是关于 的低次多项式,因此,在这一步中,我们只需要考虑 的情形如何求解(对于项数更多的情形,分别求解后求和即可)。
我们通常找一个 来替代 。这样的 具有完全积性,且在素数 处和 相同。这里考虑函数 。
Min_25 筛的第一步,我们要对于每个 ,求出数组 。其中 的定义如下:
其中 表示 的最小质因子, 表示第 个质数,。上式即前缀素数点处或最小质因子大于 处 取值之和(注意是 而非 。我们用 暂时替代了 )。
对于 ,其不存在最小质因子,故 总是 。此处可改作 ,但为了推导方便保留可能的 取值,虽然 永远不能被统计进贡献。
设 表示最大的 使得 ,那么 即为 。
考虑递推求出 。初始有 。对于 ,我们会在 的基础上删除若干个点。质数是不会被删除的,因此我们会恰好删除最小质因子是 的非质数点。
若 , 不会有变化。
若 ,形如 ,且 的最小质因子恰好是 的点会造成贡献。这可以表示为下面的式子:
来解释一下这个式子:首先,我们可以根据完全积性把 拆开。现在考虑 的取值,很显然是来源于 。但是既小于等于 又小于等于 的质数是不能作为 的,因为我们有 ,所以 ,只需要考虑 就可以了。
因此,我们得到了下式:
复杂度分析 · 一#
现在考虑复杂度。根据经典结论 ,我们对于 有 。同时可以证明 ,,因此对于任意 我们求 的时候,只会遍历 数组中第一维在 中的元素。同时,求出单个状态的复杂度是 的,因此状态数即为复杂度。
根据范围素数近似估计,复杂度可写作
注意到 ,因此 只和 差常数倍。直接将这部分提出来,上面的部分积分,得到
第二步#
综述
现在我们已经知道所有 了。将其简记为 。考虑使用这些 求出原来的解。
设 表示 ,即最小质因子大于 的 之和(我们现在抛弃 了,同时也不再限定 是质数;只不过 仍然被抛弃在外)。这样再调用 就得到答案了。
方法一
这种方法复杂度为 ,在 的时候也可以看作 ,够用。实际测试略快于第二种。这里着重介绍。
仍然考虑如何求解。分 是质数或合数两种情况讨论:
-
是质数
根据 的定义,这部分的贡献是 。
-
是合数
枚举最小质因子 ,枚举指数 ,利用积性函数的性质进行变量分离,得到
这里 有些耐人寻味。考虑前面的 没有考虑 自己的贡献。如果 ,那么 就是 ,应该归到 是质数的分类里,没有贡献。否则, 会造成 的贡献。
则 就是答案(我们之前没有考虑 的贡献)。
代码 (For Luogu P4213)
略好于之前的版本。
# include <bits/stdc++.h>
const int N=1000010,INF=0x3f3f3f3f,mod=1e9+7,inv6=166666668,inv2=500000004;
typedef long long ll;
int pr[N],pc;
bool vis[N];
int g1[N],g2[N];
ll w[N],idx1[N],idx2[N];
int wtot;
ll MAXN;
ll n;
inline int read(void){
int res,f=1;
char c;
while((c=getchar())<'0'||c>'9')
if(c=='-')f=-1;
res=c-48;
while((c=getchar())>='0'&&c<='9')
res=res*10+c-48;
return res*f;
}
inline void init(void){
for(int i=2;i<=MAXN;++i){
if(!vis[i]) pr[++pc]=i;
for(int j=1;i*pr[j]<=MAXN&&j<=pc;++j){
vis[i*pr[j]]=true;
if(i%pr[j]==0) break;
}
}
return;
}
inline int sum1(ll x){
x%=mod;
return x*(x+1)%mod*inv2%mod;
}
inline int sum2(ll x){
x%=mod;
return x*(x+1)%mod*(2*x+1)%mod*inv6%mod;
}
inline int idx(ll x){
return (x<=MAXN)?idx1[x]:idx2[n/x];
}
inline int adc(int a,int b){
return (a+b>=mod)?(a+b-mod):(a+b);
}
inline int dec(int a,int b){
return (a<b)?(a-b+mod):(a-b);
}
inline void add(int &a,int b){
a=adc(a,b);
}
inline void del(int &a,int b){
a=dec(a,b);
}
inline int mul(int a,int b){
return 1ll*a*b%mod;
}
ll S(ll x,int i){
if(pr[i]>=x) return 0;
int xid=idx(x),pid=idx(pr[i]);
ll res=dec(dec(g2[xid],g1[xid]),dec(g2[pid],g1[pid]));
for(int j=i+1;j<=pc&&1ll*pr[j]*pr[j]<=x;++j){
for(ll e=1,p=pr[j],_p;p<=x;++e,p*=pr[j]){
_p=p%mod;
res=adc(res,mul(mul(dec(_p,1),_p),S(x/p,j)+(e>1)));
}
}
return res;
}
int main(void){
scanf("%lld",&n);
MAXN=1ll*sqrt(n);
init();
for(ll l=1,r;l<=n;l=r+1){
ll val=n/l;
r=n/val,w[++wtot]=val;
g1[wtot]=dec(sum1(val),1),g2[wtot]=dec(sum2(val),1);
if(val<=MAXN) idx1[val]=wtot;
else idx2[n/val]=wtot;
}
for(int i=1;i<=pc;++i){
for(int j=1;1ll*pr[i]*pr[i]<=w[j]&&j<=wtot;++j){
int mid=idx(w[j]/pr[i]),pid=idx(pr[i-1]);
del(g1[j],mul(pr[i],dec(g1[mid],g1[pid])));
del(g2[j],mul(mul(pr[i],pr[i]),dec(g2[mid],g2[pid])));
}
}
printf("%d",adc(S(n,0),1));
return 0;
}
方法二
再度审视 的定义: 表示 ,即最小质因子大于 的 之和。
我们直接套用第一步中的递推方式。
这里的取 是不好的。我们注意到当 的时候, 没有贡献,因为这意味着 。于是可以改为
使用和第一步相同的方法进行递推即可。
至此可以使用与第一步类似的方法分析出 。
方法二的优势是,我们可以求出每个 的前缀 的答案。这无疑在整除分块需要计算 个前缀的函数值之和的时候给我们提供了便利。
代码 (For LibreOJ 6784)#
# include <bits/stdc++.h>
const int N=2000010,INF=0x3f3f3f3f,mod=1e9+7,inv6=166666668,inv2=500000004;
typedef long long ll;
int pr[N],pc;
bool vis[N];
int g1[N],g0[N];
ll w[N],idx1[N],idx2[N];
int s[N];
int wtot;
ll MAXN;
ll n;
inline int read(void){
int res,f=1;
char c;
while((c=getchar())<'0'||c>'9')
if(c=='-')f=-1;
res=c-48;
while((c=getchar())>='0'&&c<='9')
res=res*10+c-48;
return res*f;
}
inline void init(void){
for(int i=2;i<=MAXN;++i){
if(!vis[i]) pr[++pc]=i;
for(int j=1;i*pr[j]<=MAXN&&j<=pc;++j){
vis[i*pr[j]]=true;
if(i%pr[j]==0) break;
}
}
return;
}
inline int sum1(ll x){
x%=mod;
return x*(x+1)%mod*inv2%mod;
}
inline int sum0(ll x){
return x%mod;
}
inline int idx(ll x){
return (x<=MAXN)?idx1[x]:idx2[n/x];
}
inline int adc(int a,int b){
return (a+b>=mod)?(a+b-mod):(a+b);
}
inline int dec(int a,int b){
return (a<b)?(a-b+mod):(a-b);
}
inline void add(int &a,int b){
a=adc(a,b);
}
inline void del(int &a,int b){
a=dec(a,b);
}
inline int mul(int a,int b){
return 1ll*a*b%mod;
}
inline int f(int x,int k){
return x^k;
}
std::vector <int> vec;
int main(void){
scanf("%lld",&n);
MAXN=1ll*sqrt(n);
init();
for(ll l=1,r;l<=n;l=r+1){
ll val=n/l;
r=n/val,w[++wtot]=val;
g1[wtot]=dec(sum1(val),1),g0[wtot]=dec(sum0(val),1);
if(val<=MAXN) idx1[val]=wtot;
else idx2[n/val]=wtot;
}
for(int i=1;i<=pc;++i){
for(int j=1;1ll*pr[i]*pr[i]<=w[j]&&j<=wtot;++j){
int mid=idx(w[j]/pr[i]),pid=idx(pr[i-1]);
del(g1[j],mul(pr[i],dec(g1[mid],g1[pid])));
del(g0[j],dec(g0[mid],g0[pid]));
}
}
for(int i=1;i<=wtot;++i) s[i]=g1[i]=adc(dec(g1[i],g0[i]),(w[i]>=2)*2);
for(int i=pc;i;--i){
for(int j=1;1ll*pr[i]*pr[i]<=w[j]&&j<=wtot;++j){
for(ll e=1,p=pr[i];p<=w[j]/pr[i];++e,p*=pr[i]){
add(s[j],mul(f(pr[i],e),dec(s[idx(w[j]/p)],g1[idx(pr[i])])));
add(s[j],f(pr[i],e+1));
}
}
}
vec.resize(wtot);
for(int i=0;i<wtot;++i) vec[i]=adc(s[i+1],1);
std::sort(vec.begin(),vec.end());
vec.erase(std::unique(vec.begin(),vec.end()),vec.end());
int ans=0;
for(auto v:vec) ans^=v;
printf("%d",ans);
return 0;
}
小试牛刀#
例 1#
(杜教筛 例 3)
求
快进到这里。
那么我们只要瞪眼法看出后面是积性函数,直接上 Min_25 筛就完事了,不需要观察到可以卷 。
Typora 崩掉了,不写了。
作者:Meatherm
出处:https://www.cnblogs.com/Meatherm/p/18314262
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】