《一些特殊的数论函数求和问题》阅读随笔

image

好至少它教会了我如何把质数求和转化成积分的渐进 对着 π(x) 微就行了 然后直接 udv=uvvdu

18.3k……阿巴阿巴

目录

引言

这玩意挺常见的。而且你会发现有些做法能套在很多题上。咱选几个做法聊聊。
首先我讲讲拓展埃式筛求前缀和,然后再聊聊素数和,最后给你讲讲怎么用面积拟合函数。
反正不要钱,多少看一点~

1 基础知识

定义 1.1(数论函数):一个函数 f 被称为数论函数,当且仅当其定义域为正整数集,陪域为复数集。

定义 1.2(积性函数):一个函数 f 被称为积性函数,当且仅当 a,b s.t. (a,b)=1, f(ab)=f(a)f(b)

引理 1.1(数论分块):给定正整数 n,对于任意正整数 mnm 的取值只有 O(n) 种。

证明:当 mnm 的取值有 O(n) 个,而当 m>n0nmn,因此 nm 的取值有 O(n) 个。

定理 1.1(素数定理):令 π(x) 为不大于 x 的素数个数。有 π(x)=O(xlnx)

证明略。

2 积性函数求和

你发现这种题很常见。16 年任之洲同样写了积性函数求和的几种方法,并得到了一种适用范围更广的方法,又称“洲阁筛法”。事实上其更概括性的名称为“扩展埃式筛”。
但是这种筛法也有不足。首先是这个做法从 O(nlogn) 优化到 O(n34logn) 的过程中很多部分较难理解,初学者很难上手,其次是这个做法常数比较大,代码实现较复杂。但扩展埃式筛并不只有这一种做法。实际上,存在一种实现难度更低且在小范围数据下表现更好的做法。它与洲阁筛的思想类似,但理解较为容易。由于该算法由 Min_25 使用后才开始普及,其也被称为 Min_25 筛。
本文中讨论的扩展埃式筛即 Min_25 筛。

2.1 2.2 什么是 Min_25 筛?

2.3 时间复杂度

对于质数部分的计算,复杂度证明和洲阁筛完全相同。考虑每个 i=nm 只会在枚举不超过 i 的质数时产生贡献。由定理 1.1,复杂度渐进地是

i=1nO(ilogi)+i=1nO(nilogni)= O(i=1nnilogni)= O(1nnxlognxdx)= O(n34logn)

34 考虑首先将分母变成渐进的 logn,然后只需要对分子积分,而这个处理是平凡的。可以写出

O(1nnxlognxdx)=O(1nnxdxlogn)=O(n12n14logn)=O(n34logn)

然后考虑统计答案部分。

定义 2.1:对正整数 n,我们定义 big(n)n 最大的质因子,small(n)n 最小的质因子,cnt(n)n 的可重质因子个数。并令 big(1)=small(1)=, cnt(1)=0

如果我们直接按 s(n,k) 的转移式 dp,空间复杂度 O(n),时间复杂度按质数部分的分析能得到 O(n3/4logn)
如果直接暴力计算呢?在小数据的测试下,该算法似乎比 O(n3/4logn) 更优秀一些。我们尝试分析该算法的复杂度。

考虑计算 s(n,k) 时的递归。我们的时间复杂度就是进入递归的次数,即 g(n)g(pk) 被累加入答案的次数。对于从一个值开始的递归,我们不妨假设累加入的所有质数 p 处的贡献,实际上产生于 l×p 处,那我们将这次递归对复杂度的贡献计入 l×pkpk 显然是 big(l)。由于每个函数值只统计一次更新,因此每次的 l×pk 不同。同时 l×pkn 都会出现,因此该算法的复杂度就是 l×big(l)nl 的个数。

引理 2.1:对于给定的实常数 0<α<1,我们令 Q(n)={kn: big(k)kα},则 |Q(n)|nρ(1α),其中 ρ 为 Dickman 函数。

引理 2.2:对于任意 x>1ρ(x)>0x 增大而迅速衰减,ρ(x)xx

查阅此处内容可以获得关于这两个引理的信息。

定理 2.1:令 M(n)={i:i×big(i)n},则对于任意 0<α<1|M(n)|=Ω(nα)

证明:
不妨取 P={inα:big(i)i1α}。由引理知 |Q(n)|=Ω(nα)。而 xP, x×big(x)x×x1αn2αα2n,且 PM(n),于是有 |M(n)|=Ω(nα)

定理 2.2|M(n)|=Θ(n1ϵ)

证明:
我们取前 loglogn 个质数。令 pi 表示第 i 个质数,最大质因子不超过 pi 的(小于 n 的)数的个数不超过 (logn)loglognO(nloglogn)。而其他 k s.t. k×big(k)n 不超过 nloglogn,因此 |M(n)|=O(nloglogn)
由定理 2.1|M(n)|=Θ(n1ϵ)

所以这玩意是 Θ(n1ϵ) 的,但是实际跑出来的效果比较快?为啥?

引理 2.3pn, p是质数1p2=Θ(1nlnn)

证明:
进行初等积分。

pn, p是质数1p2= ndπ(x)x2= π(x)x2|nnπ(x)d(1x2)= Θ(1xlnx|n+2ndxx2lnx)= Θ(1nlnn+21n0x2d1xln1x)= Θ(1nlnn201nx2(x2)dxlnx)= Θ(1nlnn201ndxlnx)= Θ(1nlnn2 li 1n)

其中 li 为对数积分函数。由经典结论可知当 0<n<1li nnlnn。然后证完。

12 考虑 udv=uvvdu。任何发现 π() 没了的情况考虑 π(n)nlnn
为容易理解,这里的推导和论文里有些许不同。

定理 2.3:当 big(i)n14 时,满足 i×big(i)ni 至多有 O(n3/4logn) 个。

证明:
big(i)n14 时,我们枚举每个 p=big(i),至多有 np2 个满足条件的数,因此这部分贡献了 O(n3/4logn) 的复杂度。
打表可得,在 n1013 时,对于 n14 的质数 p,满足 big(i)=p 的数 i 的数量 =O(n)。只有 O(n1/4logn) 个质数,因此这部分的复杂度也是 O(n3/4logn)
这就证明了定理。

因此我们能发现,这样执行的复杂度是 O(n3/4logn) 的。

3 素数的 k 次幂前缀和

先前的算法涉及到对素数处取值求前缀和的操作。复杂度是 O(n3/4logn)。然而有时我们需要的只是 pn, p是质数pk。这时我们存在更优的做法。下面复杂度的推导中不包括求 k 次幂的影响。

定义 3.1(前缀和函数):对正整数 n,定义 πk(n) 为素数的 k 次幂前缀和函数,即 πk(n)=pn, p是质数pk。特别的,π0(x) 即为素数计数函数 π(x)

定义 3.2(部分筛函数):对正整数 n,a,定义部分筛函数 ϕk(n,a)=i=1n[small(i)>pa]ik。当 a=0ϕk(n,a)=i=1nik

定义 3.3(特定筛函数):对正整数 n,a,定义特定筛函数 Ps,k(n,a)=i=1n[small(i)>pa,cnt(i)=s]ik

n13pa=Bx12 时,可以枚举质因子得到 ϕk(n,a)=P0,k(n,a)+P1,k(n,a)+P2,k(n,a)=1+πk(n)πk(pa)+P2,k(n,a)

πk(n)=ϕk(n,a)+πk(pa)P2,k(n,a)1
由于 panπk(pa) 可以直接筛出来。我们现在只需要考虑 ϕk(n,a)P2,k(n,a) 的计算。

3.1 关于 P2,k(n,a)

考虑 s=2i 能贡献 ik,当且仅当 i=pq,且 pa<pqnp, B<q<nB。枚举 p 后计算 q 的贡献即可。
复杂度 O(nB)

3.2 关于 ϕk(n,a)

引理 3.1ϕk(n,a)=ϕk(n,a1)pakϕk(npa,a1)

显然。

定理 3.1ϕk(n,a)=big(i)pa,small(i)>pbμ(i)ikϕk(ni,b)

就是把引理 3.1 多用了几次。
不难(?)导出 ϕk(n,a)=big(i)paμ(i)iknik

首先朴素计算 npa=B 的贡献,然后只需要考虑 big(i)pa<iμ(i)iknik 的贡献。奇妙结论:

big(i)pa<iμ(i)iknik=B<iB×small(i)μ(i)ikϕk(ni,π0(small(i))1)

这式子可以建出递归对应的二叉树得到。

换元。令 p=small(i),m=i/pμ(i)=μ(m),也就导出了

pBsmall(m)>p,mB<mpμ(m)(mp)kϕk(nmp,π0(p)1)

阈值法。

3.2.1 pB

可以朴素求解。我们计算出所有 ϕ,随后枚举 p,m 统计。
由引理 1.1,有效状态是 nBlogB 的。

otherwise p>B
如果 m 不是素数,则由于 small(m)>pm>p2>B,这样的 m 不存在。
因此我们只需要考虑 m 为质数的情况。也就是说下面的讨论中 μ(m)=1
改变求和式为

pBm>p,mB<mp(mp)kϕk(nmp,π0(p)1)

3.2.2 p>n13

nmp<n13<p,则根据定义有 ϕk(nmp,π0(p)1)=1
改变求和式为

n1/3<pBpkm>p,mB<mpmk

我们可以枚举 p,求 m 的贡献。筛出质数 k 次方和计算。复杂度 O(B)

3.2.3 B<pn13

直接拿定义冲

B<pn1/3m>p,mB<mp(mp)kϕk(nmp,π0(p)1)= B<pn1/3m>p,mB<mp(mp)kmprn,small(r)prk= r=1nBrkB<pn1/3m>p,mB<mp(mp)k[mprn,small(r)p]= r=1nBrkB<pmin(n1/3,small(r))pkp<mmin(npr,b)mk

于是问题来到了维护这边。
枚举 p,他能贡献的 r 满足 small(r)p,这里使用树状数组维护。数论分块维护 npr 得到 m 的贡献。在树状数组上查询的次数是 O(np) 的。

于是问题来到了复杂度这边。

首先是修改。

定理 3.2:对于任意 0<α<1n 以内恰有 k1 个质因子且最小质因子不小于 nα 的数的个数是 O(nlogkn) 的。

证明:
k=1 即为定理 1.1
归纳法。假设 k<k0 的所有情况都成立,现在证明 k=k0 时同样成立。
进行初等积分:

O(nαnnplogk1npdπ(p)))= O(nplogk1npplogp|nαn)O(nαnplogpdnplogk1np)= O(nlogkn)O(nαnnplogp(1plogk1np)dp)= O(nlogkn)O(nαnnplogp(lognp+k1p2logknp)dp)= O(nlogkn)+O(nαnnplogplogk1npdp)= O(nlogkn)

感想:积分是用来分析的,不是用来创人的。
34 用渐进擦除了 k1,把 log 前的负号提了出去。

归纳成立。

upd: 这个似乎是错的,因为积分出了问题,实际上应当趋于 O(n/logn)

我们在树状数组上进行操作的时候需要保证 r 的最小质因子不小于 B。有用的 r 只有 O(nBlogn) 种。乘入树状数组的复杂度后为 O(nB)

然后是查询。

定理 3.3pmnp=O(mnlogm)

证明:
进行初等积分:

pmnp= npm1p= O(n1mdπ(p)p)= O(n(π(p)p|1m1mπ(p)d1p))= O(n(π(m)m+121m1plnpdp))= O(nπ(m)m)= O(mnlogm)

这个挺好理解。

然后带入 m=n13 可知询问次数是 O(n2/3logn) 的。带上树状数组就是 O(n2/3)

到这里分析完了计算 ϕ 的所有情况,总时间复杂度总结一下就是 O(n23+nB+nBlogB),取 B=n13log23n 得到后半部分复杂度是 O((nlogn)2/3),总复杂度还是 O(n2/3)

3.3 优化

可以发现复杂度瓶颈在 B<pn13 的查询部分。
于是问题又来到了维护这边。

抄式子:

r=1nBrkB<pmin(n1/3,small(r))pkp<mmin(npr,b)mk

我们进一步利用那个定理。分别考虑 r 是否为质数:
如果 r 是质数,若 rn13,则对所有满足条件的 p 来说 r 都有贡献。因此枚举 p,将质数部分统计完即可。复杂度 O(n2/3logn)
otherwise rp2,且存在 q 需要使得 xr>p2,则 px14。此时由定理 3.3 可知询问次数为 O(x5/8logx),这部分不会影响复杂度。

因此喜提 O((nlogn)2/3) 的总复杂度。

指路【模板】Meissel–Lehmer 算法
指路 OI Wiki
最大的收获是重学积分(

4 约数函数前缀和

4.1 题面

定义 σ(i)i 的约数个数,给定正整数 n<263,求 i=1nσ(i)

4.2 朴素转化

i=1nσ(i)= x=1nd|x1= d=1nnd

考虑转化

i=1nni= i=1nj=1n [ijn]( xy=n 线)= i=1nj=1ni1+i=1nj=1ni1i=1nj=1n1= (xy=n  x[1,n] 线)+(y[1,n] )()= 2i=1nni(n)2

问题转化为拟合 xy=n 下方的整点数。咋拟合呢?想到

4.3 Stern-Brocot TreeFarey 序列

《具体数学里面应该有。》
不不不我还会说几句的

定义 4.1Farey 序列):将分母不超过 n01 的最简分数从小到大排成一个序列,我们称它为 nFarey 序列。如果存在一个 Farey 序列使得 p,q 连续出现,则我们称这两个数字为 Farey neighbour

引理 4.1:任意 ab>cdFarey neighbour,当且仅当 adbc=1

证明:自己查。

在本文中,若 ab>cdFarey neighbour,则称 ba>dc 也是 Farey neighbour。容易发现引理 4.1 在此情况下仍然成立。

定义 4.2Stern-Brocot Tree:见此处

image

4.4 利用 Stern-Brocot Tree 切割曲线

每次用一条直线切割,统计直线下方的整点数。初始化就是经过 (n,n) 且斜率为 1 的直线。

得到一条直线后计算整点数可以直接用皮克定理,不展开。

4.4.1 坐标系的转换

考虑计算 S(x0,y0,a,b,c,d)。由于曲线在 PRPQ 上方,考虑令 PR,PQ 分别为 u,v 轴。某个方向 +1 相当于是向这个方向走到下一个接触到的整点。

定义有 (a,b)=(c,d)=1,因此对于 (u,v),其在原坐标系下就是

x=x0+udvb

y=y0+ucva

由上,有 ax+by=ax0+by0+(adbc)u。由于 abcdFarey neighbour,有 adbc=1。因此 u 是整数。施于 v 得到 v 也是整数。

自然想到找 xy=n 的对应。不妨令 w1=ax0+by0,w2=cx0+dy0,则 x0=dw1bw2,y0=aw2cw1。于是 x=d(u+w1)b(v+w2),y=a(v+w2)c(u+w1)

曲线即为 (u+w1)(v+w2)cd(u+w1)2ab(v+w2)2=n。由于 w1,w2,cd,ab0,该曲线上 u>0v>0 一一对应。设 u=U(v),v=V(u)。我们能发现 U,V 在第一象限内都是形似 xy=n 的凹函数。可以求导证明。

4.4.2 计算整点数

我们仍然找一条切凹函数且斜率为 1 的直线,统计它下方的整点数。我们设切点为 G,原系坐标是 (Gx,Gy),新系坐标是 (Gu,Gv)。随后找到两个整点 S,T 使得这两点在 u 坐标上极小地夹住 G。形式化地,SuGu<Su+1=TuSv=V(Su),Tv=V(Tu)。这两点显然存在且不同。

S,T 作切线的平行线 SA,TB 分别交 v 轴于 A、交 u 轴与 B。并设 M(Ax,Ay+1)C(Bx+1,By),作 MN 平行 SA 交曲线于 N。然后把折线 ASTB 左下方(被染上橙色的区域,包括边界)的整点统计入答案。这段图形整体上斜率为 1,带换到 xOy 坐标系下斜率为 a+cb+d,而通过 Stern-Brocot Tree 二分可以得到 ab,a+cb+d,cdFarey neighbour,这样可以接着切割了。

我们断言,任何未被统计的点,要么出现在 MN 上方,要么出现在 AC 右侧。因此可以递归 S(Bx,By,a,b,a+c,b+d)+S(Cx,Cy,a+c,b+d,c,d) 求解。

为啥这里是 (Bx,By) 不是 (Mx,My)

4.5 时间复杂度

据说这个算法可以被证到 O(n13logn),但是论文中只证到 O(n13log3n)。但总而言之,现在能够证明的是该算法的复杂度为 O~(n13)

一篇论文中给出了一种 O(n13) 的做法:A Successive Approximation Algorithm for Computing the Divisor Summatory Function, Richard Sladkey, 2012

论文里代码的实现

不保证正确性,代码效率低下

namespace calc {
	int n;
	double C1 = 4000, C2 = 10;

	int delta(int i) { return i * (i + 1) / 2; }
	
	int SR(int w, int h, int a1, int b1, int c1, int a2, int b2, int c2) {
		int s = 0, a3 = a1 + a2, b3 = b1 + b2;
			
		auto H = [&](int u, int v) {
			return (b2 * (u + c1) - b1 * (v + c2)) * (a1 * (v + c2) - a2 * (u + c1));
		};
		auto U_tan = [&] { 
			return (int)(sqrtl( (a1 * b2 + b1 * a2 + 2 * a1 * b1) * (a1 * b2 + b1 * a2 + 2 * a1 * b1) * n / (a3 * b3) ) ) - c1;
		};
		auto V_floor = [&](int u) {
			return (int)(((a1 * b2 + a2 * b1) * (u + c1) - ceil(sqrtl((u + c1) * (u + c1) - 4 * a1 * b1 * n))) / (2 * a1 * b1) - c2);
		};
		auto U_floor = [&](int v) {
			return (int)(((a1 * b2 + a2 * b1) * (v + c2) - ceil(sqrtl((v + c2) * (v + c2) - 4 * a2 * b2 * n))) / (2 * a2 * b2) - c2);
		};

		auto SI = [&](int i_max, int p1, int p2, int q, int r) {
			int s = 0, A = p1 * p1 - 2 * r * n, B = p1 * q, C = 2 * p1 - 1;
			rep(i, 1, i_max - 1) {
				C += 2, A += C, B += q;
				s += (B - floor(sqrtl(A))) / r;
			} return s - (i_max - 1) * p2;
		};
		auto SW = [&]() {
			return SI(w, c1, c2, a1 * b2 + a2 * b1, 2 * a1 * b1);
		};
		auto SH = [&]() {
			return SI(h, c2, c1, a1 * b2 + a2 * b1, 2 * a2 * b2);
		};

		if (h > 0 and H(w, 1) <= n) {
			s = s + w, c2 ++, h --;
		} 
		if (w > 0 and H(1, h) <= n) {
			s = s + h, c1 ++, w --;
		}
		if (w <= C2) return s + SW();
		if (h <= C2) return s + SH();
		int u4 = U_tan(), v4 = V_floor(u4), u5 = u4 + 1, v5 = V_floor(u5);
		int v6 = u4 + v4, u7 = u5 + v5;

		auto SN = [&]() {
			return delta(v6 - 1) - delta(v6 - u5) + delta(u7 - u5);
		};
		
		s += SN();
		s += SR(u4, h - v6, a1, b1, c1, a3, b3, c1 + c2 + v6);
		s += SR(w - u7, v5, a3, b3, c1 + c2 + u7, a2, b2, c2);
		return s;
	}

	int calc() {
		int x_min, x_max, y_min;
		x_max = floor(sqrtl(n));
		y_min = n / x_max;
		x_min = min(x_max, ceil(C1 * pow(2 * n, 1. / 3)));
		lll s = 0;
		int a2 = 1, x2 = x_max, y2 = y_min, c2 = a2 * x2 + y2;

		while (true) {
			int a1 = a2 + 1;
			int x4 = floor(sqrtl(n / a1)), y4 = n / x4, c4 = a1 * x4 + y4;
			int x5 = x4 + 1, y5 = n / x5, c5 = a1 * x5 + y5;
			if (x4 < x_min) break;

			auto SM = [&]() {
				return delta(c4 - c2 - x_min) - delta(c4 - c2 - x5) + delta(c5 - c2 - x5);
			};
			s = s + SM();
			s = s + SR(a1 * x2 + y2 - c5, a2 * x5 + y5 - c2, a1, 1, c5, a2, 1, c2);
			a2 = a1, x2 = x4, y2 = y4, c2 = c4;
		} 

		auto SQ = [&](int x1, int x2) {
			int s = 0, x = x2, bet = n / (x + 1), eps = n % (x + 1), delt = n / x - bet, gam = bet - x * delt;
			while (x >= x1) {
				eps += gam;
				if (eps >= x) {
					delt ++, gam -= x, eps -= x;
					if (eps >= x) {
						delt ++, gam -= x, eps -= x;
						if (eps >= x) break;
					}
				} else if (eps < 0) {
					delt --, gam += x, eps += x;
				} 
				gam += 2 * delt, bet += delt, s += bet, x --;
			} 
			eps = n % (x + 1), delt = n / x - bet, gam = bet - x * delt;
			while (x >= x1) {
				eps = eps + gam;
				int delt2 = eps / x;
				delt += delt2, eps -= x * delt2;
				gam = gam + 2 * delt - x * delt2, bet += delt, s += bet, x --;
			} 
			while (x >= x1) {
				s += n / x, x --;
			} 
			return s;
		};

		auto S1 = [&]() {
			return SQ(1, x_min - 1);
		};
		auto S2 = [&]() { 
			return (x_max - x_min + 1) * y_min + delta(x_max - x_min); 
		};
		auto S3 = [&]() {
			int ret = 0;
			rep(x, x_min, x2 - 1) 
				ret += (n / x) - (a2 * (x2 - x) + y2);
			return ret;
		};

		s = s + S1() + S2() + S3();
		return 2 * s - x_max * x_max;
	}
}

int paper(int n) {
	calc::n = n;
	return calc::calc();
}

4.6 优化

我们发现,我们其实没有必要费时费力地切割原面积。其实问题可以转化为从 (n,n) 这个点开始拟合 xy=n 的下半部分。我们需要拟合的面积是下图中红色虚线下方的阴影部分:

假设我们现在拟合到了 (x,y),我们要选择一个不在阴影内且在当前点右侧的点 (p,q),满足 qypx 最大且 p 最小的点,并将 (x,y)(p,q) 的线段加入折线,在这过程中加入折线下整点数。
我们可以维护一棵 01,11 为初始值的 Stern-Brocot Tree。每次拿出栈顶分数 pq,从 (x,y) 移动到 (x+a,yb)(我们也可以将分数看作一个向量)。就这么一直移动直到再走一步会进入阴影中。然后把栈首弹出。接着对新的栈首执行上述操作直到栈首 L 不行,第二个元素 R 行的时候。
这时我们将栈首弹出,开始二分最接近斜率。
我们在 Stern-Brocot Tree 上走,设当前 mid=(Lx+Rx,Ly+Ry),考虑当 (x,y) 加入 mid 时是否会进入阴影。
如果不会进入则 mid 入栈,R=mid,接着二分。如果会进入,讨论一下。如果加入后斜率 R,那再怎么加都没用了,可以直接退出了。反之就令 L=mid
栈的用处就是回溯到第一个合法位置。

zzt 的实现

然后复杂度和先前那个做法的证法类似,是 O(n13logn)
Min_25SPOJ 说他无法给出一个证明,灵感来源 PE540 的讨论区也无法给出。
但是他推测这个曲线的凸包上有 Θ(n13logn) 种斜率(每个形如 [n2k+1,n2k] 的区间上有 O(n) 种。
所以他推测这种算法的时间复杂度有界 Ω(n13log3n)
为了保证复杂度,当前点的 y<n13 时停止迭代,开始朴素做法。

4.7 拓展

这里除了凸性外没有用到 xy=n 的任何性质,也就是说这个做法可以自然推广到其他奇怪的凹曲线内部整点计数。

posted @   joke3579  阅读(372)  评论(5编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示