\newcommand{\floor}[1]{\left\lfloor #1 \right\rfloor}这里我讲讲如何用 Dirichlet series 求一些算术函数的前缀和。这个算法我是在 Project Euler 的论坛里看到的,Lucy_Hedgehog 的帖子。但是他(她?)写的比较粗略,我就稍微写详细一点吧……
Preliminary
这里我们定义几个 notation,并交代几个基础性质。名字什么的我都是乱翻译的,可能和已有的名字不同,如果看到了请告知。
算术函数(Arithmetic function) 若函数 f 定义域为 N+,值域为 C,则称其为算术函数。定义为其前缀和函数 Sf(n)=x=1∑nf(x)。
我们的问题是给定 N,如何快速求 Sf(N),特别地,我们希望时间复杂度是 sublinear 的。
积性函数(Multiplicative function) 若算术函数 f 满足:如果 (n,m)=1,则有 f(n)f(m)=f(nm),则称 f 为积性函数。易知所有积性函数 f 满足 f(1)=1。不过这篇文章没有用到任何积性函数的性质……
Dirichlet series 给定算术函数 f,定义其 Dirichlet series 为
Df(s)=n=1∑∞nsf(n).
举几个例子:
\quad D_\boldsymbol{1}(s) = \sum_{n=1}^\infty \frac{1}{n^s} = \zeta(s), \quad D_\text{Id}(s) = \sum_{n=1}^\infty \frac{n}{n^{-s}} = \zeta(s - 1).
Dirichlet 卷积(convolution) 定义两个算术函数 f,g 的 Dirichlet convolution 为
(f∗g)(n)=d∣n∑f(d)g(n/d).
容易验证以下事实:
- Dirichlet 卷积满足交换律、结合律。
- 令 ε(n)=[n=1],则对于任意算术函数 f,均有 ε∗f=f。
较为经典的几个例子有
(ϕ∗1)(n)=d∣n∑ϕ(d)=n=Id(n).
以及
(μ∗1)(n)=d∣n∑μ(d)=[n=1]=ε(n).
Dirichlet 逆元(inverse) 若 f∗g=ε,则称 g 为 f 的 Dirichlet 逆元,记做 g=f−1。这里提供了很多逆元的例子。上面提到 μ 的 Dirichlet 逆元就是 1。Möbius 反演可以简单理解为
f∗1=g,⇔f=g∗μ.
注意 Dirichlet 逆元不一定存在。
Dirichlet convolution 与 Dirichlet series 令 f,g 为两个算术函数,有
Df∗g(s)=n=1∑∞ns1d∣n∑f(d)g(n/d)=a,b≥1∑(ab)sf(a)g(b)=a=1∑∞asf(a)b=1∑∞bsg(b)=Df(s)Dg(s).
即 f∗g 的 Dirichlet series 为 f 的 Dirichlet series 乘上 g 的 Dirichlet series。
算法 high-level idea
我们先固定一个 N∈N。对于一个算术函数 f,我们 somehow 维护一个数据结构来表达 Df(s),并且这个数据结构支持以下操作:
- 给定 Df(s) ,能快速计算 Sf(N)。注意这个数据结构不需要对于任何一个 n 都给出 Sf(n)。
- 对于一些较为“简单”的算术函数 f,能够快速初始化这个数据结构。
- 给定 Df(s) 和 Dg(s),能快速计算 Df+g(s),Df−g(s),Df∗g(s),Df/g(s)。
那么我们就可以把目标算术函数 f 的 Dirichlet series 用若干个较为“简单”的算术函数的 Dirichlet series 的四则运算表达出来,然后直接查询 Sf(N) 即可。
算法详情
这个数据结构非常简单:令 w 为一待取常数,我们记录所有的 {f(n)}n=1w 以及所有的 {Sf(⌊iN⌋)}i=1⌊N/w⌋。这样查询非常简单,因为我们已经记录了 Sf(N)。接下来我们介绍如何支持第三个操作。由于 Df+g(s),Df−g(s) 过于 trivial,我就懒得写了……
计算 Df∗g(s)
不妨令 h=f∗g。对于 {h(n)}n=1w,直接用定义暴力就好了,复杂度是 O(wlogw)。
对于 {Sh(⌊iN⌋)}i=1⌊N/w⌋ 这一块,其实也挺好整的:对于任意 x∈N+,
Sh(x)=n≤x∑h(n)=a,b:ab≤x∑f(a)g(b)=a≤x∑f(a)b≤x/a∑g(b)=a≤x∑f(a)Sg(⌊ax⌋).\labeleq:Sc
由于 x=⌊N/i⌋,所以实际上我们已经有 Sg(⌊x/a⌋) 了,且所有不同的 ⌊x/a⌋ 只有 O(x) 个。故我们使用经典 trick,可以枚举出所有不同的 ⌊x/a⌋,对一个区间内的 x 利用 Sf 一次计算完毕。这样计算一个 Sh(⌊N/i⌋) 的复杂度为 O(N/i),计算出所有 Sh(⌊N/i⌋) 的复杂度为:
i=1∑⌊N/w⌋N/i≈∫i=1N/wN/idi=O(N/w).
这里直接用 Dirichlet hyperbola method 也行,复杂度不变:
Sh(x)=a≤x∑f(a)Sg(⌊x/a⌋)+a≤x∑g(a)Sf(⌊x/a⌋)−Sf(⌊x⌋)Sg(⌊x⌋).
整体复杂度为 O(wlogw+N/w)。
计算 Dh/f(s)
过程和 Df∗g(s) 基本上类似。
令 g=h/f。对于 {g(n)}n=1w,可得
d∣n∑f(d)g(n/d)=h(n)⇒f(1)g(n)=h(n)−d∣n,d>1∑f(d)g(n/d).
故依次计算 {g(n)}n=1w 即可。复杂度依然是 O(wlogw)。写法上可能需要注意。
对于第二部分,由 \eqrefeq:Sc 可以直接推出
f(1)Sg(x)=Sh(x)−1<a≤x∑f(a)Sg(⌊ax⌋),
复杂度分析类似乘法,也是 O(wlogw+N/w)。
从这个算法也可以看出,f−1 存在当且仅当 f(1)=0。
取 w
很显然,我们只要让 wlogw=N/w,即 w=Θ((N/logN)2/3),就可以使得整体复杂度取得最小,为 O(N2/3log1/3N)。
这个算法里面我没有仔细想 O(wlogw) 这部分是否可以优化到 O(w),如果可以的话复杂度可以再降一个 O(log1/3N)。比起这篇文章来说,复杂度降低了 O(N1/12/log1/3N),可喜可贺,可喜可贺。不过我感觉那篇文章的参数选的有问题,他用的 w=N1/2……
“简单”的算术函数
对于一些函数,我们能够直接求出它的 Dirichlet series 表示形式,这些函数就可以作为算法的起点。常用的有以下几个例子:
- Idk(n)=nk 的 Dirichlet series 为 ζ(s−k),可以直接初始化该数据结构。k 大的时候可能需要 Faulhaber 公式。
- 若 f 只在少数几个输入上非 0,则也可以直接初始化该数据结构。
简单例题
D_\phi(s) = \frac{D_{\text{Id}}(s)}{D_\boldsymbol{1}(s)} = \frac{\zeta(s - 1)}{\zeta(s)}.
于是 ϕ 的前缀和可以快速搞定。
μ∗1=ε,⇒Dμ(s)=ζ(s)1,
于是 Mertens function 也可以在 O(N2/3log1/3N) 内搞定。注意到我们在算 Dμ(s) 的时候,{μ(n)}n=1w 这一块可以直接线性筛出来而不是暴力用除法搞,这样这部分的复杂度可以从 O(wlogw) 降到 O(w),取 w=N2/3 就可以把整体时间复杂度降到 O(N2/3),和经典方法一样了。
这类算法最大的问题是如何用简单的算术函数通过给定的运算表达出目标算术函数,我试了几个题,表示有点难……主要是 Dirichlet inverse 根本看不出有啥性质……
对 Project Euler 有兴趣的同学可以尝试 PE 351/415/428/447/448/608/715,还可以看一下 Lucy_Hedgehog 和 MuthuVeerappanR 在 428 下的 post,以及 715 下 Jaehyun 的 post。我猜 MuthuVeerappanR 就是刚才提到的那篇 blog 的作者。
代码的话我找时间 push 到 github 上去吧……
这文章写的比之前的《解析方法数质数:从入门到入土》轻松多了……