2024-04-20 17:25阅读: 318评论: 2推荐: 4

Number Theory

202404

0 前言

离散数学是本书的重点,而整数又是离散数学的中心议题。数论 是讨论整数性质的重要数学分支,因此我们要来探索它。

​ ——《具体数学》第四章


标有 * 的为拓展内容,或者说比较难的题目,但它们都非常有趣。

部分题目的代码是洛谷的提交记录,阅读这篇文章的大多数同学都该能看(若有需要可以找我要)。

没看懂的地方直接问,不排除我会存在手误。


1 基本概念

1.1 整除

两个整数相除,我们自然想去讨论商是否是一个整数。小学生常常期待这个,因为整数比分数写起来好看。

这引出了数论中的基础概念:整除

如果 m>0n/m 是一个整数,我们就说 m 整除 n(或者 nm 整除)。这个性质奠定了整个数论的基础,所以我们赋予它一个特殊的记号会更方便,记为

mnm>0kn=mk

同样的,如果 m 不整除 n,我们就写成 mn


1.2 整值函数

整数是离散数学的支柱,我们常常需要将分数或者任意的实数转换到整数。 ——《具体数学》第三章

我们首先来讨论 (floor,最大整数)函数和 (ceiling,最小整数)函数,对于所有实数 x,其定义如下:

x=xx=x

俗称 下取整上取整,底和顶也会在数论中有许多应用,在后续的部分内容中我们也会用到。


xx 之间的差称之为 x分数部分,它在应用中经常出现,所以也值得拥有自己的记号:

{x}=xx

我们有时称 xx整数部分,因为 x=x+{x},这也是一个实数的表示方法。


尝试在这里就证明一个非常有趣且有用的事实:如果 mn 是整数且分母 n 为正,则

x+mn=x+mnx+mn=x+mn

f(x) 是任意一个具有如下性质且在一个实数区间连续的单调递增函数

f(x)=x=

于是只要 f(x),f(x),f(x) 有定义,我们就有

f(x)=f(x)f(x)=f(x)

画出图来容易发现这是显然的,这里不做证明。

我们发现其实 f(x)=x+mn 也具有上面要求的 f(x) 的性质,于是我们就证得了上面的结论。


1.3 带余除法

mn 是正整数时,nm 除的商是 n/m。而大多数情况我们都不能做到 m 整除 n,所以这个除法的余数也有一个简单的记号,很方便,我们称它是 nmodm。基本公式

n=mn/m+nmodm

告诉我们,可以将 nmodm 表示成 nmn/m,我们可以将它推广到任意实数:

xmody=xyx/y,y0

y=0 呢?为了避免用 0 作除数,也为了完整起见,我们可以定义:

xmod0=x

这里的 mod 就被定义成了一个二元运算,也就是 取模。容易发现 xmody 是在 [0,y1] 中的一个值。

我们约定 mod 比加法或者减法的优先级高。同时,分配律mod 最重要的代数性质,对于所有实数 c,x,y,我们有

c(xmody)=(cx)mod(cy)

容易从定义证明这个法则,因为如果 cy0,则有

c(xmody)=c(xyx/y)=cxcycx/cy=cxmodcy

且模数为零时该式显然也为真(两边都是 cx)。


关于 mod,还有一些非常显然的性质。

  • (a+b)modp=((amodp)+(bmodp))modp
  • (ab)modp=((amodp)(bmodp))modp
  • amodbkmodb=amodb

前两条定义了 mod 的加法和乘法,第三条则是关于模数的一个转化。

这些性质都是耳熟能详的,这里也不做证明。


1.4 同余关系

模运算是数论提供的一种重要工具,我们在上面把它当成二元运算使用,而在这里我们也可以把 mod 运用到整个方程上面,为此,我们使用稍不同的记号会更加方便:

ab(modm)amodm=bmodm

由于 xmodmx 相差 m 的倍数,因而我们可以用另一种方式来解读同余式:

ab(modm)abm

同余符号 看起来很像 =,因为同余式与方程非常相像。例如,我们将同余式元素相加减,仍保持同余关系:

abcda±cb±d(modm)

乘法同样有效,只要处理的对象是整数:

abcdacbd(modm)

证明直接作差:acbd=(ab)c+b(cd)。反复利用乘法性质,我们可以取幂:

abanbn(modm),a,b,n0

这样一来,我们对方程所习惯做的大多数代数运算对同余式都可以运用,但并不是所有运算。除法运算显然不在其中。

以下部分可能会用到之后要讲的内容。

如果 adbd(modm),我们不能总断言 ab(modm)。例如 3×25×2(mod4),但 35(mod4)

然而在 dm 互素的情形中,我们可以挽救这一消元的性质:

adbd(modm)ab(modm),a,b,d,m,dm

它的证明是直接在式子两边乘上 d 的逆元,进而,我们可以把这个东西推广到更为一般的法则,它尽可能小地改变模:

adbd(modm)ab(modmgcd(d,m))

进一步观察改变模的想法,我们可以得到另外一些式子:

ab(modmd)ab(modm)

反过来,如果我们知道对于两个小的模数有 ab,是否能断定对于一个更大的模数有 ab 呢?这个规则是

ab(modm)ab(modn)ab(modlcm(n,m))

因为如果 abmn 的倍数,就一定是 lcm(n,m) 的倍数。

注意关注这个式子的特殊情形 mn,这在之后的 中国剩余定理 颇有应用。


2 欧几里得

有着我们熟悉的 gcd(a,b)lcm(a,b)

2.1 欧几里得算法(gcd)

欧几里得算法,又称辗转相除法,常用递归得到最大公因数。

gcd(a,b)=gcd(b,amodb)

而这是容易简单证明的:

a=kb+ra,b 的最大公因数为 d,那么一定满足 d|a,d|b

于是 r=akb,我们将两边同时 ÷d 就可以得到 rd=adkbd

由于 d|a,d|b,所以上面式子的右边一定是整数,于是 rd 也是一个整数。

这样一来,d 也是 amodb=r 的因数,得证。


CF1806F2 GCD Master (hard version)

给定 n,m 和一个长度为 n 的序列 {ai}(aim)

定义一次对一个长度为 m 的序列的操作为,选择序列中两个下标 1i<jm,删去 aiaj,然后在序列末端加入 gcd(ai,aj)

例如,对于 [7,6,2],一次操作可以选择下标 23,这样操作后,序列变成 [7,2]

给定 k,求对序列 {ai} 执行 k 次操作后得到序列中的数的和的最大值。

1K<n106,1m1018

感谢🐨的供题。


首先我们考虑有相同的数如何处理?

发现我们对相同的数操作,相当于删去一个,进而在后面的讨论中我们发现其实相同的数并无意义,所以我们最后枚举删了几个相同的数就可以了,于是我们就只需要对不相同的数进行操作即可。


考虑每一次操作,删除两个数 x,y 并加入 gcd(x,y),而当我们再把 gcd(x,y)z 操作时就等价于删除 x,y,z 并加入 gcd(x,y,z)

那么这个问题就被转化成把数分成若干组,删去这一组并加上它们的 gcd,对于一组 k 个元素,它们需要的操作次数是 k1 次。

我们如何让分组最优?不难猜到把所有的分成一组。

假设现在我们存在两组 ST,它们的 gcd 分别是 x,y 满足 xy,那么此时的贡献是

x+yiSaiiTai

我们尝试合并两组,也就是把 S 中最大的元素拿走,gcd 变成 gcd(x,y),由于数都是不同的,所以 S 中的 mx2x,那么现在的贡献就是

gcd(x,y)+mxiSaiiTai

由于 mx2x,所以

gcd(x,y)+mx>x+y

一定是成立的,所以合并两组一定更优,于是我们就得到了这一结论。


这样我们就可以取枚举 gcd,选出其和最小的集合,再枚举一下删了多少个相同的数,直接减去它们的和就可以在 O(mlogm) 的时间复杂度解决问题。(我们发现把相同的数加到删除的集合和直接删除是本质相同的,所以若删去 p 个相同的数,我们只需要把前 p 小的提出来删掉,具体可以见代码的 b 数组)


但在这道题中,依次枚举 gcd 很明显是不能行的,那么我们希望有一个方法去调优,假设当前选了 b1,b2,,bt 这样的 t 个数,按照升序排序,当前的 gcdd,我们考虑替换掉一个数,是否可以使得答案更优?

假设我们现在加入一个没有选的最小的数 ax,用它来换掉 bt,那么贡献就是

btaxd+gcd

因为元素互不相同,所以 btbt1+d,如果 ax<bt1,那么我们就有

btaxd+gcdbtbt1d+gcd>0

于是这样的调整一定是更优的。


这就意味这什么呢?

假设我们选了 t 个数,那么有原数组的前 t1 小值。

也就是说我们只需要枚举选了多少个数和最大的数选的是什么就可以了。但这样还是会 T。


考虑把 a 排序之后做一个前缀的 gcd,容易知道它一定是不断递减且成阶梯状的,段数是 O(logm) 级别的,所以你只需要枚举当前的前缀 gcd 是那个段,每一段暴力处理一遍,时间复杂度就是 O(nlogm) 的。代码


双倍经验:CF1806F1 GCD Master (easy version)


2.2 扩展欧几里得算法(exgcd)

扩展欧几里得算法,即 exgcd,旨在求不定方程 ax+by=gcd(a,b) 的一组整数解 x,y


关于一个不定方程 ax+by=c 是否有解,我们需要证明一个引理:裴蜀定理

如果 a,b 均为整数,则有整数 x,y 满足 ax+by=gcd(a,b)

证明是简单的,我们设 d=ax+by 容易发现 d 一定是 gcd(a,b) 的倍数,那么最小的 d 就是 gcd(a,b)

容易发现,如果 ax+by=cc 不是 gcd(a,b) 的倍数,这个不定方程是无解的,反之一定有解。


同时,根据 裴蜀定理 我们可以得到一个推论:

两个整数 a,b 互质,当且仅当 ax+by=1 存在解 x,y


那如何求这组方程 ax+by=gcd(a,b) 的解呢?

我们考虑欧几里得算法的递归过程 gcd(a,b)=gcd(b,amodb)

当递归结束时 b=0,我们返回 a,此时有 ax+by=gcd(a,b)x=1,y=0

所以我们可以尝试先递归去求 bx+(amodb)y=gcd(b,amodb)

那么

ax+by=gcd(a,b)=gcd(b,amodb)=bx0+(amodb)y0=bx0+(aabb)y0=b(x0aby0)+ay0x=y0,y=(x0aby0)

于是这就可以在欧几里得算法中解决了。

int exgcd(int a,int b,int &x,int &y){
  if(b==0) return x=1,y=0,a;
  int d=exgcd(b,a%b,y,x);
  y-=a/b*x;return d;
}

那么我们如何从一组解推广到更多组解呢?(这样可以方便找到一组最小正整数解)

我们用 exgcd 得到了一组解 x,y,满足 ax+by=gcd(a,b)

对于 x,y 的两边我们分别 kb,+ka,容易发现 xkb,y+ka 也是一组合法解,进而,我们将 ka,kb 分别 ÷gcd(a,b) 得到的 kb/gcd(a,b),+ka/gcd(a,b) 依然是一组合法解。

所以 x 的最小正整数解是 (x%m+m)%m 其中 m=b/gcd(a,b)

通过这样的调整,我们就可以找到想要的解了。


而回到最初,如果 gcd(a,b)|c,我们把最后的答案 x,y 直接乘上 cgcd(a,b) 即可。


P1082 [NOIP2012 提高组] 同余方程

求关于 x 的同余方程 ax1(modb) 的最小正整数解。

整理一下方程,即将同余拆开,我们可以得到

ax1(modb)ax+by=1

那么下面这个东西是直接可以用 exgcd 解决的,于是你就做完啦!


那如何找到最小整数解呢?

根据上面的转化,其实就是找到最小的 xkb,于是直接用 xb 取模即可。代码


然后你就会求逆元了!


ABC340F S = 1

给你整数 XY ,它们至少满足 X0Y0 中的一个。

请找出一对满足以下所有条件的整数 (A,B) 。如果不存在这样的一对,请报告。

  • 1018A,B1018
  • xy 平面上,顶点为 (0,0),(X,Y),(A,B) 的三角形的面积为 1

1017X,Y1017

机翻的,还能看。一场非常近的 ABC,有些人在场上。


考虑如何计算这三个点组成的三角形面积呢?

用四边形面积直接减去三个角上的三角形,于是可以得到

ayab2xy2(ax)(yb)2=aybx2

而由于我们只考虑了一种象限的情况,所以 S=|aybx2|

题目要求 |aybx2|=1,则 |aybx|=2,于是可以直接用 exgcd 得到一组特解了。Hanghang 的代码代码


2.3 类欧几里得算法

f(a,b,c,n)=i=0nai+bc

其中 a,b,c,n 是常数,我们需要一个 O(logn) 的算法。


这个式子和《具体数学》3.5 中的例三相当像啊,也就是说当 n=c 时,这个式子是具有封闭形式的。

但是先不急,我们先把类欧几里得算法推完再来讨论这个问题。


如果 ac 或者 bc,那么我们可以通过取模以减小范围。

i=0nai+bc=i=0n(acc+amodc)i+bcc+bmodcc=acn(n+1)2+bc(n+1)+i=0n(amodc)i+bmodcc=acn(n+1)2+bc(n+1)+f(amodc,bmodc,c,n)

我们就把 ac 或者 bc 的情况转化成了 a<c,b<c 的情况了!


接下来,用一些处理和式和处理底和顶的技巧

 i=0nai+bc=i=0nj=0ai+bc11=j=0an+bc1i=0n[j<ai+bc]

m=an+bc,则

j=0m1i=0n[j<ai+bc]=j=0m1i=0n[j+1ai+bc]=j=0m1i=0n[j+1ai+bc]=j=0m1i=0n[ai>jc+cb1]=j=0m1i=0n[i>jc+cb1a]=j=0m1(njc+cb1a)=mnf(c,cb1,a,m1)

这就是一个递归的式子了,交换了 a,c 的位置,于是又可以重复上次的过程:先取模,再递归。

这也就是欧几里得算法的辗转相除过程,于是类欧几里得名字就是这样来的。


接下来我们对于类欧几里得算法进行一个推广,尝试去求另外两个和式:

g(a,b,c,n)=i=0niai+bch(a,b,c,n)=i=0nai+bc2


首先来推导 g,用上面类似的方法,先来考虑 ac 或者 bc 的情况。

g(a,b,c,n)=i=0niai+bc=i=0ni(acc+amodc)i+bcc+bmodcc=acn(n+1)(2n+1)6+bcn(n+1)2+i=0ni(amodc)i+bmodcc=acn(n+1)(2n+1)6+bcn(n+1)2+g(amodc,bmodc,c,n)

接下来考虑 a<c,b<c 的情况,和上面 f 较为类似.

g(a,b,c,n)= i=0niai+bc=j=0an+bc1i=0n[i>jc+cb1a]i

t=jc+cb1a,m=an+bc,那么

=j=0m1i=0n[i>t]i=j=0m1(n+t+1)(nt)2=12(mn(n+1)j=0m1t2j=0m1t)=12(mn(n+1)h(c,cb1,a,m1)f(c,cb1,a,m1))


接着,我们推导 h,方法也是类似的。

考虑 ac 或者 bc 的情况。

h(a,b,c,n)=i=0nai+bc2=i=0n(acc+amodc)i+bcc+bmodcc2=i=0n(aci+bc+(amodc)i+bmodcc)2=i=0n(aci)2+i=0nbc2+2i=0nacbci+2i=0n(aci+bc)(amodc)i+bmodcc+i=0n(amodc)i+bmodcc2=ac2n(n+1)(2n+1)6+bc2(n+1)+acbcn(n+1)+2acg(amodc,bmodc,c,n)+2bcf(amodc,bmodc,c,n)+h(amodc,bmodc,c,n)

考虑 a<c,b<c,但是平方并不好处理,于是我们对其进行一个转化

n2=2n(n+1)2n=2i=0nin

那么

h(a,b,c,n)=i=0nai+bc2=2i=0n(j=0ai+bcj)f(a,b,c,n)

还是设 m=an+bc,t=jc+cb1a,则对前面的进行求和

i=0n(j=0ai+bcj)=i=0nj=0ai+bc1(j+1)=j=0m1(j+1)i=0n[j<ai+bc]=j=0m1(j+1)i=0n[i>t]=j=0m1(j+1)(nt)=nm(m+1)2j=0m1(j+1)t=nm(m+1)2g(c,cb1,a,m1)f(c,cb1,a,m1)

三个函数一起进行递归,于是你就可以通过 P5170 【模板】类欧几里得算法 啦!代码

const ll mod=998244353,i2=499122177,i6=166374059;
ll T,a,b,c,n;

struct node{
  node () {f=g=h=0;}
  ll f,g,h;
}ans;

node calc(ll a,ll b,ll c,ll n){
  ll ac=a/c,bc=b/c,m=(a*n+b)/c,in=n*(n+1)%mod*(2*n+1)%mod*i6%mod;
  node res;
  if(a==0){
  	res.f=bc*(n+1)%mod;
  	res.g=bc*n%mod*(n+1)%mod*i2%mod;
  	res.h=bc*bc%mod*(n+1)%mod;
  	return res;
  }
  if(a>=c||b>=c){
  	res.f=n*(n+1)%mod*i2%mod*ac%mod+(n+1)*bc%mod;res.f%=mod;
  	res.g=in*ac%mod+n*(n+1)%mod*i2%mod*bc%mod;res.g%=mod;
  	res.h=ac*ac%mod*in%mod+bc*bc%mod*(n+1)%mod+ac*bc%mod*n%mod*(n+1)%mod;res.h%=mod;
  	node e=calc(a%c,b%c,c,n);
  	res.h+=e.h+2*bc%mod*e.f%mod+2*ac%mod*e.g%mod;
  	res.g+=e.g;res.f+=e.f;
  	res.h%=mod;res.f%=mod;res.g%=mod;
  	return res;
  }
  node e=calc(c,c-b-1,a,m-1);
  res.f=(n*m%mod-e.f+mod)%mod;
  res.g=(m*n%mod*(n+1)%mod-e.h-e.f+mod+mod)%mod*i2%mod;
  res.h=(n*m%mod*(m+1)%mod-2*e.g%mod-2*e.f%mod-res.f%mod+3*mod)%mod;
  return res;
}

P5179 Fraction

给你四个正整数 a,b,c,d,求一个最简分数 pq 满足 ab<pq<cd

若有多组解,输出 q 最小的一组,若仍有多组解,输出 p 最小的一组。

a,b,c,d109

一道用到类欧类似思想的题目。


什么时候我们可以不加思考地直接算出这个目标分数?

发现当 ab<1,cd>1,我们直接返回 11 就可以了,那么现在我们考虑如何进行转化。

举一个例子:

19117<pq<1485

对其取倒数,可以得到

11719>qp>8514

把两边 1 的部分换掉

114<q6pp<319

我们把这两边的分数变成更小的真分数,继续循环:

13<p6(q6p)q6p<81

从而一步一步带回即可。代码


这道题中我们把假分数转成真分数的操作和类欧类似。


ABC283Ex Popcount Sum

T 组询问,每组求 1n 内, modmr 的数在二进制下 1 的个数的和。

1T105,1mn109

x 在二进制下 1 的个数进行简单转化,代码中常用 (x>>i)&1 那么转化成数学表达式就是

i=0log2x(x2i2x2i+1)

而由于我们只统计 mj+r,于是整个式子其实就是

j=0nrmi=0log2mj+r(mj+r2i2mj+r2i+1)=i=0log2nj=0nrm(mj+r2i2mj+r2i+1)

这个东西就是容易用类欧几里得算法在 O(Tlog2n) 的时间复杂度内求出了。代码


*当 n=c1 时的封闭形式

我们来尝试求

k=0m1nk+xm,m>0n

的封闭形式,也就是 fn=c 的情况。这是有趣的,可以教给我们一些其他的东西。


前置知识:来自《具体数学》3.25 和 3.26。

n=nm+n+1m++n+m1m

如何证明这个东西?

你可以看成把 n 个物品分成 m 组,那么一定有一些组是 nm 个物品,另外一些就是 nm 个。

而从小到大排好序后,第 i 组刚好有 n+i1m 个元素。


同时,我们还可以对这个东西进行推广,用 mx 替换 n,就可以得到一个对所有实数 x 都成立的恒等式:

mx=x+x+1m++x+m1m


首先可以考虑打表,或许你可以发现一些东西,具体可以去看《具体数学》3.5。

而推导时我们可以用刚才推导类欧几里得类似的方法:对 nk 取模。

0k<mnk+xm=0k<mnkmm+nkmodm+xm

把它拆成整数部分和小数部分就是

0k<m(x+nkmodmm+nkmnkmodmm)

考虑把三项分开来求和,对于第一项和第三项,我们都涉及到了 knmodm

那么证明处理这个神秘东西呢?

容易发现这是简单的,设 d=gcd(n,m),根据上面同余关系中的一些证明,则

anbn(modm)adbd(modmd)

于是它所得到的就是把按照某种次序排列的数 0,d,2d,,md 重复 d 次,在后面我们会更为细致地讨论它。

所以这个式子的第一部分的和就是

d(xm+x+dm++x+mdm)=d(x/dm/d+x/d+1m/d++x/d+m/d1m/d)=dxd

最后一步就用到了上面前置知识中的东西。


而对于第三部分,我们发现它其实就是一个等差数列重复了 d 次,所以它的和就是

d(12(0+mdm)×md)=md2

中间的第二部分,也是非常简单的,同样是一个等差数列,它的和就是

m12n

于是我们要求的封闭形式就找出来了

0k<mnk+xm=dxd+m12n+dm2

如果对封闭形式稍作处理,就可以让它关于 m,n 对称,变成

dxd+(m1)(n1)2+d12

就得到这样的互反律

0k<mnk+xm=0k<nmk+xn

类欧几里得算法,完。


3 同余关系

在前置知识中提到了同余关系的相关法则,那么在这里我们希望对它的理解更为深刻一些。


3.1 费马小定理

费马小定理:

np11(modp),p

我们知道,p1 个数 nmodp,2nmodp,,(p1)nmodp 就是数 1,2,,p1(按照某种次序排列),于是我们把他们乘在一起就是

n×(2n)××((p1)n)(nmodp)×(2nmodp)××((p1)nmodp)(p1)!

这个式子是在 modp 的意义下成立的,所以我们可以得到

np1(p1)!(p1)!(modp)

由于 (p1)!p 是互素的,所以两边可以同时 ÷(p1)!,证毕。


同时,费马小定理还有一个更通用的表达方式

npn(modp),n


3.2 逆元

如果一个线性同余方程 ax1(modb) 有解,则乘 xamodb 的逆元,记作 a1


容易发现,这个东西可以用上面我们讨论过的 exgcd 直接求解,同时当 b 是质数时,我们也可以直接用 费马小定理 直接快速幂解决。

这样两种方法都可以做到单次求逆元 O(logn) 的时间复杂度。

而根据前面我们对 exgcd 是否有解的讨论,容易发现 a 存在逆元的充要条件是 ab


可是有些时候每次都单独求逆元并不能满足我们所需要的时间复杂度,所以这里就有了另外两种求多个数逆元的方法:

  • 线性求逆元

    首先我们知道 11=1(modp),我们尝试线性推出 i1

    那么令 k=pi,j=pmodi,于是 p=ki+jki+j0(modp)

    我们将式子两边同时乘上 i1j1 可以得到:

    kj1+i10(modp)i1kj1(modp)i1pi(pmodi)1(modp)

    这样就可以做到线性递推求逆元了。时间复杂度 O(n),就可以通过 P3811 【模板】模意义下的乘法逆元 啦!

  • 线性求 n 个数 ai 的逆元:利用前缀积求逆元

    sa 的前缀积,那么如果我们知道了 sn 的逆元,于是可以从 n 倒推回去:si1=si+11×ai

    这样每个数的逆元就是 ai1=si1×si1,时间复杂度 O(n+logp),这种方法在求阶乘的逆元中常常用到。


3.3 二次探测定理

二次探测定理:

如果 p 是一个奇素数,则方程 x21(modp) 仅有两个解:x=1x=p1


我们先不急着讨论这个东西,先来讨论 x21(modm) 有多少个解?


首先我们应该考虑 m 是素数幂 pk 的情形,这里 k>0,此时同余式 x21 可以写成

(x1)(x+1)0(modpk)

所以 p 必定整除 x1 或者 x+1,或者整除它们两者。但是 p 不可能同时整除 x1x+1,除非 p=2,如果 p>2,那么 pk(x1)(x+1)pk(x1)pk(x+1),所以恰好有两个解 x1x1

p=2 的情形稍有不同。如果 2k(x1)(x+1),那么 x1x+1 中的一个能被 2 整除,但不能被 4 整除,所以另外一个必定能被 2k1 整除。

这就意味着当 k3 时我们有四个解,即 x±1x2k1±1


现在,x21(modm) 当且仅当对 m 的完全分解式中所有满足 mp>0 的素数 p 都有 x21(modpmp)

每个素数都是独立于其他素数的,对于 xmodpmp,除了 p=2 的情形,皆有两种可能性。

于是如果 mr 个不同的素因子,则 x21 的解的总数是 2r,除了当 m 是偶数时要做修正之外,一般来说,精确的解数是

2r+[8m]+[4m][2m]


在这个推导过程中,我们已经证明了 二次探测定理 了!


3.4 威尔逊定理

接下来引出一个逆元的应用——威尔逊定理,他会在之后有关素数的判定中有所应用。

(n1)!1(modn)n,n>1

这个定理的一半是平凡的:如果 n>1 不是素数,它就有一个素因子 pp 必然也是 (n1)! 的一个因子,所以 (n1)! 不可能与 1 同余。

具体来说,在 1.4 我们引入了一个改变模数的式子,也就是如果 (n1)! 对模 n1 同余,那么就应该与模 p1 同余,但是事实并非这样。


威尔逊定理的另一半说的是,(p1)!1(modp),我们可以用 逆元配对 来证明这一半的结论。

因为 p 是素数,如果 np,那么就一定存在一个 n 满足

nn1(modp)

这里 nn 的逆元,n 也是 n 的逆元。所以如果 nn,那么它们两个就可以进行配对,从而直接抵消掉了。


那哪些数的逆元就是它本身呢?

容易发现其实就是问 x21(modp) 的解,这就是上面我们证明的二次探测定理。

p 是奇素数时,只会有 x=1x=p1,那么整个式子就是 (p1)!(p1)1(modp),证毕。

p=2 时,直接带进去也是非常显然的,这样我们就证明出来了。


3.5 有趣的遗留问题

在上面推类欧几里得算法的一个拓展时,我们希望证明 m 个数

0modm,nmodm,2nmodm,,(m1)nmodm

按照某种次序恰好组成了 md 个数

0,d,2d,,md

d 分复制,其中 d=gcd(n,m)


或许上面已经给出了一些不太严谨的简要证明,我们在这里希望把它写得更完整一些。

首先证明得到前面 md 个值的 d 份复制是简单的,根据 1.4 同余关系的一些等式,我们可以知道

jnkn(modm)j(nd)k(nd)(modmd)

从而当 0k<md 时,我们得到这些值的 d 份复制.


那么现在我们必须指出,那 md 个数就是 {0,d,2d,,md} 按照某种次数排列。

我们记 m=md,n=nd,那么根据 1.4 中的分配律就有 knmodm=d(knmodm),所以当 0k<m 时出现的那些之是 d 去乘

0modm,nmodm,2nmodm,,(m1)nmodm

而这种情形就是 mn 的情形。

根据 鸽巢原理,我们就可以轻松证明出这 m 个数就是 0,1,,m1,这个遗留问题就解决了!


它可以被理解成在一个环上面每次跳 n 步能跳到的点,在 OI 中也会有许多应用。


4 线性同余方程组

求解形如

{xa1(modn1)xa2(modn2)xak(modnk)

线性同余方程组,在 OI 中有着广泛的应用。


4.1 中国剩余定理(CRT)

中国剩余定理全称 Chinese Remainder Theorem,简称 CRT。它用于求解 模数两两互质 的线性同余方程组,即对于任意 ij,都有 ninj


首先我们来看一个例子:

在《孙子算经》中有这样一个问题:“今有物不知其数,三三数之剩二(除以 32),五五数之剩三(除以 53),七七数之剩二(除以 72),问物几何?

CRT 的步骤是这样的:

  1. 找出三个数:

    35 的公倍数中找出被 7 除余 1 的最小数 15

    37 的公倍数中找出被 5 除余 1 的最小数 21

    75 的公倍数中找出被 3 除余 1 的最小数 70

  2. 15×22 为最终除以 7 的余数),用 21×33 为最终除以 5 的余数),用 70×2

  3. 把以上三个数相加得到 233,再用它除以 3,5,7 的最小公倍数 105 ,得到余数 23 即为最小满足条件的数。


它为什么对的呢?

我们假设 m1 是一个 2(mod3) 的数, m23(mod5) 的数, m32(mod7) 的数。

m1 的角度出发,已知 m12(mod3),如果需要 (m1+m2+m3)2(mod3) ,那么 m2,m3 都是 3 的倍数。

同理我们可以得到:

  1. m1 除以 32,且是 57 的公倍数。
  2. m2 除以 53,且是 37 的公倍数。
  3. m3 除以 72,且是 35 的公倍数。

所以 CRT 用数学表示出来就是:

N=n1×n2××nkNi 表示 N÷ni

x(a1N1N11+a2N2N21++akNkNK1)(modN)

因为 NiNi1Ni1 是关于 modni 的逆元) 就满足了是其他数的公倍数,且 1(modni) ,于是再用 ai 乘上它即可。


P1495 【模板】中国剩余定理(CRT)代码

#include<bits/stdc++.h>
using namespace std;
#define ll long long

const int N=15;
int n;
ll p,a[N],b[N],m,x,y,ans=0;

void exgcd(ll a,ll b ,ll &x,ll &y){
  if(b==0){x=1,y=0;return;}
  exgcd(b,a%b,y,x);y-=a/b*x;
}

int main(){
  scanf("%d",&n);m=1ll;
  for(int i=1;i<=n;i++){
  	scanf("%lld%lld",&a[i],&b[i]);
  	m*=a[i];
  }
  for(int i=1;i<=n;i++){
  	p=m/a[i];
    exgcd(p,a[i],x,y);
    ans+=b[i]*p*(x<0?x+a[i]:x);
  }
  printf("%lld\n",ans%m);
  return 0;
}

4.2 扩展中国剩余定理(exCRT)

如果就是单纯的 线性同余方程组 怎么解呢?也就是 n 并不两两互质。

这就是 exCRT 所解决的问题,它能解决的东西更为普遍,故使用范围也会更广。


先考虑只有两个方程的简单情况。

{xa1(modn1)xa2(modn2)

容易发现 x=a1+n1k1=a2+n2k2,我们把右边的两个式子提出来并进行移项

n1k1n2k2=a2a1

那么这就是一个可以用 exgcd 求解的方程了。


如果 gcd(n1,n2)a2a1,那么线性同余方程组无解。

否则,我们可以得到新的方程,也就是合并了两个方程:

a=a1+n1k1,n=lcm(n1,n2)

推广到 n 个方程的情况,这样依次合并下去,最后的答案就是合并出来的 a 了。

我们就成功求解了 线性同余方程组


P4777 【模板】扩展中国剩余定理(EXCRT)代码

ll mul(ll a,ll b,ll m){
  ll res=0ll;
  while(b){if(b&1) res=(res+a)%m;a=(a+a)%m;b>>=1;}
  return res;
} 

ll exgcd(ll a,ll b, ll &x,ll &y){
  if(b==0){x=1,y=0;return a;}
  ll d=exgcd(b,a%b,y,x);y-=(a/b)*x;return d;
}

ll wrk(){
  ll ans=a[1],M=b[1];
  for(int i=2;i<=n;i++){
    ll A=M,B=b[i],C=(a[i]-ans%B+B)%B;
    ll gcd=exgcd(A,B,x,y),bg=B/gcd;
    if(C%gcd!=0) return -1;
    x=mul(x,C/gcd,bg);
    ans+=x*M;M*=bg;
    ans=(ans%M+M)%M;  	
  }
  return (ans%M+M)%M;
}

5 素数

接下来我们来讨论素数。


5.1 素数的例子

如果一个正整数 p 恰好只有两个因子,即 1p ,那么这个数就称为 素数

而其他那些还有非平凡因子的数都称为 合数,每一个大于 1 的整数要么是素数,要么是合数。


任何正整数 n 都可以表示成素数的乘积:

n=p1pm=k=1mpk,p1pm

算数基本定理 表示:仅有一种方法将 n 按照素数非减的次序写成素数的乘积。(证明略)


关于素数的其他定理:

  • n 个素数 Pn 大约是 n 的自然对数的 n 倍:

    Pnnlnn

  • 对于不超过 x 的素数个数 π(x):

    π(x)xlnx

证明过于复杂,这里不给出证明,大家可以自行了解。


还有一种与素数相关的数:梅森数

形如

2p1

的数,其中 p 总表示素数。

如果 n 是合数,那么 2n1 不可能是素数,因为 2km12m1 作为一个因子:

2km1=(2m1)(2m(k1)+2m(k2)++1)

但是当 p 是素数时,2p1 并不总是素数,2111=2047=23×89 是最小的这类非素数。


5.2 互素关系

gcd(n,m)=1 时,整数 mn 没有公共的素因子,我们就称它们是 互素的

我们写成

mnm,ngcd(i,j)=1

而一个分数是最简分数,当且仅当 mn,通常我们通过消去分子分母的最大公约数来简化成最简分数,于是我们有

mgcd(n,m)ngcd(n,m)

那我们有没有什么好的方法来构造满足 mn 的全部非负的分数 mn 组成的集合呢?

答案是肯定的,他被成为 Stern-Brocot 树,其思想是从两个分数 (01,10) 出发,然后依照你希望的次数重复下面的操作

  • 在两个相邻的分数 mnmn 之间插入 m+mn+n

新的分数就是 m+mn+n 称为 mnmn中位分数,例如

01,11,10

接下来就是

01,12,11,21,10

于是我们把这个分数阵列可以看成是一棵无线的二叉树构造,就是这样

image

每一个分数都是 m+mn+n,其中 mn 是位于左上方且离它最近的祖先,而 mn 则是右上方离它最近的祖先。


为什么这种构造是对的呢?为什么每次的 m+mn+n 是最简分数?而又只会出现一次?


因为我们发现,在这个构造中任意一个阶段的相邻分数都满足

mnmn=1

首先这个关系在 1×10×0=1 是成立的,而每一次插入一个新的中位分数 m+mn+n 时,我们需要检验的就是

(m+m)nm(n+n)=1m(n+n)(m+m)n=1

括号展开就一样了,而这也让我们发现

mn<m+mn+n<mn

一个中位分数不一定在原先两个的中间,但它的确位于它们两个中间的某个地方,于是我们就不可能得到相同的分数。


但是我们现在仍然存在一个问题——会遗漏么?

这个问题时简单的,由于它是无限的,所以我们会不断用上面的单调性去逼近它,这样就不会漏。


这里我们再引入一个概念,阶为 N 的法里级数 Fn,它是介于 01 之间的分母不超过 N 的所有最简分数组成的集合,且按照递增的次序排列,那么当 N=6 时就是

Fn=01,16,15,14,13,25,12,35,23,34,45,56,11

容易发现它是 S-B 树 的子树,好像也没什么好讲的。


事实上,我们可以把 S-B 树 看成一个表示有理数的 数系,因为每一个正的最简分数都恰好出现一次。

我们用字母 L,R 表示它在这一层往哪边走,就可以唯一确定一个数。

我们记当前的字符串为 S,那么就有

f(S)=S

例如 f(LRRL)=57

这个东西和矩阵乘法的构造是比较像的,也就是我们尝试去构造矩阵

M(s)=(nnmm)

那么我们知道

M(SL)=(nn+nmm+m)=(nnmm)(1101)

然后我们就发现其实

L=(1101),R=(1011)

于是我们就可以通过一个 LR 字符串定位出它所定义的值了。


还有一种方法就是改变 m,n,而不是保持状态 S,我们发现

f(RS)=f(S)+1

因为 RSS,只是将上面一行加到了下面一行上,也就是

S=(nnmm),RS=(nnm+nm+n)

类似的性质对 L 也有,于是我们就有

{mn=f(RS)mnn=f(S)mn=f(LS)mnm=f(S)

这样的定位就可以不用矩阵乘法。


关于互素的讨论到此结束,更多可以参见《具体数学》!


P8058 [BalkanOI2003] Farey 序列

把所有分子和分母都 n最简真分数 从小到大排成一行,形成的序列称为 Farey 序列。

求出 n 所对应的 Farey 序列中第 k 小的数。

2n4×104,1k 符合条件分数的个数。

sto smb orz

给出一个 smb 的亚线性做法。


有了上面的一些简单铺垫,我们不难想到在 S-B 树 上面进行二分。

那么对于一个最简分数 xy,有多少个数比它小呢?

容易列出式子

i=1nj=1i[gcd(i,j)=1][jixy]

对这个式子进行莫反,可以得到

i=1nj=1i[gcd(i,j)=1][jixy]=i=1nj=1idi,djμ(d)[jixy]=d=1nμ(d)i=1ndj=1i[jixy]=d=1nμ(d)i=1ndj=1i[jixy]=d=1nμ(d)i=1ndixy

我们发现前面是可以用杜教筛处理的(其实这道题中不用),而后面这一块是类欧几里得的板子,于是这个值我们是可以在 O(n23+nlogn) 的时间内完成的。


而我们如何去二分到这个值呢?

不难想到直接在 S-B 树 上面走,每次枚举走到左子树还是走到右子树,但是在 k=1 的情况我们会走 n 步,很明显是不满足条件的。

我们考虑把往一个方向走的连续段缩起来,也就是只枚举在哪些地方拐(eg:原先一直往左子树走,现在变成往右子树走),设 F(n) 表示拐 n 次能到达的分母最小值,那么容易得到 F(n)=F(n1)+F(n2),也就是一直拐 n 次,那么左边就是 F(n1),右边是 F(n2)

那么这样我们只会拐 logn 次,每次走多长可以用倍增实现,于是我们就做完了。

总时间复杂度 O(n23+nlog3n)代码


ABC333G Nearest Fraction

给定一个小于 1 的正实数 r 和一个正整数 n。 要求在满足 0pqngcd(p,q)=1 的前提下,找到使 |rqp| 最小的二元组 (p,q) 。 如果存在多个这样的二元组 (p,q),输出 qp 值最小的那个。

数据范围:1n1010r(0,1) 且最多包含 18 位有效数字。

首先 r 容易被转化成分数形式,把它设为 m,我们考虑用 S-B 树 来解决这个问题。


对于当前的区间 [l,r],我们考虑在树上二分来找到最近的位置,那么每一次一定是形如先从 r 一直往左跳到第一个 <m 的地方,再把这个点设成左端点,又不断往右跳。

这样我们就可以不断减小这个区间,从而不断逼近 m

每一次搜索都用两个答案 ans1,ans2 分别记录在 m 两侧得到的最近点,这样就做完了。

用上一道题类似的倍增优化可以做到 O(log2n)代码


双倍经验:P1298 最接近的分数


SP26073 DIVCNT1 - Counting Divisors

σ0(i) 表示 i 的约数个数。

S(n)=i=1nσ0(i)

多测,T105,n263

比较难的应用。


首先我们可以对式子进行转化

S(n)=i=1nσ0(i)=i=1nni=i=1nj=1n[i×jn]=i=1nj=1n[i×jn]+i=1nj=1n[i×jn]i=1nj=1n[i×jn]=2i=1nnin2

那么问题就转化成求

i=1nni

我们尝试把这个反比例函数画出图来,那么我们要求的其实就是下图中红色部分中的点数(from x义x):

image

不妨将其称作 R 区域。


我们希望用一些向量去不断拟合这个反比例函数,于是就想到了这个有理数系:S-B 树

于是我们需要一个单调栈,里面维护的向量斜率单调递减,进行这样的操作:

  1. 每次从栈顶取出一个元素,不断加到当前点 (x,y) 上面,直到它差一步就走进 R 区域。

由于我们保证了斜率递减,且向量的斜率表示出来已经是最简分数,所以每一步都是对答案有贡献的,也就是图中棕色区域中的点(from x义x):

image

容易发现这样的点数一定不会多算,可以做到不重不漏。(在斜线下的点一定在反比例函数中,并且不会多)

如何计算点数?(我们假设每次上都要计算最下面的一行,从而不能计算上面的一行)

画出图来就是这个样子:

image

把它分成蓝色和橙色的两个部分,而橙色部分一定是上下对称的,所以假设当前向量为 L,当前起点在 (x,y),终点在 (x+L.x,yL.y),则贡献是

xL.y+(L.x1)(L.y+1)2

  1. 不断弹出栈顶,使得攻栈顶刚好走不进 R 区域,称它为 r。设最近一次弹的是 l(有可能是上一步的栈顶向量)。

现在就是,加上 l 刚好走进 R,加上 r 刚好走不进 R,就是这个样子(from x义x):

image

  1. 开始二分,用 S-B 树 得到中间向量 (lx+rx,ly+ry),我们称之为 mid
  • 如果 mid 走一步不会进 R,那么 mid 进栈并把 r=mid 继续二分。

  • 否则

    • 如果 rf(x+xmid),那么继续二分肯定都走不出 R 了,直接停止二分,就是这个样子(from x义x):

image

可以发现如果 rf(x+xmid)mid,r 的中间向量 midmid 显然也一定会进 R,而且因为 f 单调增,rf(x+xmidmid) 也一定满足。

- 否则 $l=mid$ 继续二分。

yn3 时可以直接暴力。

这样的时间复杂度是 O(n3logn)。(笔者不会证)


注意到其他的一些东西:

  • 最开始的点我们选择 (nB,B+1),因为这样才能做到不重不漏(细节会少很多)。

  • 初始栈中的向量是 (1,0)(1,1)

这样我们就做完了。代码


5.3 素数筛法

最重要的知识点,几乎所有题目都需要筛出 1n 的所有素数。


首先我们存在一个非常显然的做法——埃氏筛法

埃氏筛用所有已经筛出的素数倍数标记合数:从 2n 枚举所有 i,若 i 未被标记,则 i 是素数,将 ii 以外的倍数标记为合数。

埃氏筛的精髓在于复杂度的证明:不超过 n 的指数的倒数之和为 O(lnlnn) 级别。

也就是

pP,pn1p=O(nlnlnn)

这说明埃氏筛的时间复杂度是 O(nlnlnn)


这里是来自 djq 的证明:

因为每个数只会被其素因子筛到,所以

pP,pn1p=i=1nω(i)

根据 d(i) 的计算式

i=1n2ω(i)i=1nd(i)=O(nlnn)

根据 2x 的凸性和琴生不等式得

i=1n2ω(i)n2i=1nω(i)n2i=1nω(i)nO(lnn)

两边同时取对数

i=1nω(i)nO(lnlnn)

因此

i=1nω(i)O(nlnlnn)


埃氏筛已经非常接近与线性了,但是它也还不是最优的,我们存在一个 线性筛素数 的做法。

它的思想和埃氏筛类似,埃氏筛的优秀之处在于仅用质数的倍数筛去合数,但合数会被多个质因子筛到。让每个合数仅被晒一次就能做到线性。

考虑用每一个合数 最小质因数 筛它:从 2n 枚举所有数 i。对于每个 i,令其最小的质因子为 p,则对于不大于 p 的质数 qiq 的最小质因子为 q。将所有 iq 标记为合数,则每个合数 c 仅在 i=cm 时以 im 的形式晒去,其中 mc 的最小质因子。

综上,有如下步骤:

  • 从小到大遍历当前筛出的所有素数 prj,将 i×prj 标记为合数。

  • prji,退出循环,因为 prji×prk(k>j),所以 i×prk 的最小质因子为 prj 不是 prk,再筛就重复了。

时间复杂度线性,于是就可以通过 P3383 【模板】线性筛素数

  for(int i=2;i<N;i++){
  	if(!vis[i]) prm[++cnt]=i;
  	for(int j=1;j<=cnt&&prm[j]*i<N;j++){
  	  vis[prm[j]*i]=true;
  	  if(i%prm[j]==0) break;
  	}
  }

5.4 素数判定(Miller Rabin 素性测试)

如何判断一个数是否是素数?

小素数的判定是容易的:直接试除法即可,时间复杂度 O(n)

至于大素数判定……


费马曾提出一种方法:费马素性测试

它基于费马小定理:设 n 是素数, a 是正整数且与 n 互素,那么有 an11(modn)

为了测试 n 是否是素数,我们在 1n 中随机选择一个基数 a ,而 a 不需要与 n 互素。

  • 如果 an11(modn) 不成立,那么 n 肯定不是素数。

  • 如果 an11(modn) 成立,那么 n 很大概率是素数,尝试的 a 越多, n 是素数的概率就越大。


但是从第 2 种情况看出来费马素数测试不是完全正确的。

对于某个 a 值,总有一些合数被误判通过了测试;不同的 a 值,被误判的合数不太一样。

特别地,有一些合数,不管选什么 a 值,都能通过测试,这种数叫 Carmicheal 数 (561,1105,1729,)

不过这种数很少,1 亿以内只有 255 个,而越到大的越稀疏,所以费马素性测试几乎不会出错,编码也很简单。


由于上面 费马素性测试 的不完美,我们就引入了 Miller-Rabin 素性测试

把费马素性测试改进一下,它是已知最快的随机素数测试算法。

原理也非常简单,就是费马素性测试排除 Carmicheael 数。

因为并且 Carmicheael 数在进行次数较多的二次探测定理逆否命题判定后,都会被判定出来。


如果 n>2,且 n 是奇数,测试它是否为素数。

根据费马测试,如果 an11(modn) 不成立,一定不是素数。

而由于 n1 过大,我们考虑一个技巧:

n1 表示成幂的形式,令 n1=2tu 其中 u 是奇数,t 是正整数。

于是有:

an1(au)2t(modn)

所以计算出 au 依次乘二,这样每次平方同时使用二次探测定理进行判定。


容易发现,对于多个随机的基值 a,假设随机了 s 个,做了 s 次测试,出错的概率是 2s

s=50 时,出错的概率已经可以忽略不计了。

考虑做了 s 次运算,每次做 t 次快速幂,在 n 很大的时候速度极快!

ll mul(ll a,ll b,ll m){ll z=(long double)a/m*b,res=(a*b-z*m)%m;return (res+m)%m;}
ll add(ll a,ll b,ll n){return (a+b>=n)?a+b-n:a+b;} 
ll qpow(ll a,ll b,ll m){
  ll res=1ll;
  while(b){
  	if(b&1) res=mul(res,a,m);
  	a=mul(a,a,m);
  	b>>=1;
  }
  return res;
}

const ll prm[12]={2,3,5,7,11,13,17,19,23,29,31};
bool miller_rabin(ll n){
  if(n<2) return false;
  for(int i=0;i<=10;i++){
  	if(n==prm[i]) return true;
  	if(n%prm[i]==0) return false;
  } 
  ll u=n-1,t=0;
  while(!(u&1)) u>>=1,t++;
  for(int i=0;i<=10;i++){
    ll a=prm[i],x1=qpow(a,u,n),x2;
    for(int j=1;j<=t;j++){
      x2=mul(x1,x1,n);
      if(x2==1&&x1!=1&&x1!=n-1) return false;//二次探测定理
      x1=x2;
	}
	if(x1!=1) return false;
  }
  return true;
}

5.5 分解质因数(Pollard-rho)

判断完了素数,我们来尝试分解质因数。

众所周知,可以用试除法做到 O(n),但是同样 n 一旦大了就不行了。


所以我们考虑一种更高消的方法——Pollard-rho 启发式方法 分解质因数。

事实上,我们还有一种奇妙的方法来寻找一个合数的因子(from TQX):

scanf("%d",&n);
vector<int> fac;
for(int i=1;i<=???;++i){
	int a=rand()%(n-1)+1;
	if(n%a==0) fac.push_back(a);
}

这个代码试图通过随即一些数并判断它们是否是 n 的因数,很明显这是一个非常蠢的方法,因为单次操作找到 n 的因数的概率实在是太小了。

这个算法非常的劣质,但我们的 Pollard_rho 正是基于这个算法而来的,它的核心思想就是随机。


而我们知道 生日悖论

如果现在有 1000 个数,在里面随机到 37 的概率非常小,但如果我们找两个数 i,j 使 |ij|=37 的概率就要大一倍;

而当 k=30 时,概率已经超过 50% ,当 k=100 时,概率已经到了 99.99%


这个悖论告诉我们:组合随机采样比单个个体满足条件要高得多,这样可以提高正确率。

于是,Pollard 就提出了一个随机数的生成方法:

f(x)=(x2+c)modn

其中 c 可以随机生成,在随机生成了一个 x1 后, x2=f(x1),x3=f(x2),

这个函数的随机性非常好,但是进行了数次生成之后就会出现一个环,在坐标系中表示出来就变成了这个样子:

image

发现这个图像很像 ρ,于是这就是它的名字由来。


那么考虑如何判环?

可以用 Floyd 的方法:

假设我们有 2 个函数生成的值 a,b,我们每次让 a 走一步, b 走两步,当 a,b 相等时就出现了环。

于是我们可以用这样随机出来的数相邻两个的差与 n 取 gcd 来分解 n,即如果得到的 gcd 1 ,那么就直接返回。

但是 gcd 的时间复杂度时 O(logn) ,每一次都去调用一定会很慢。

在计算时,我们将每次产生的 abc(ab) 相乘并积累下来,最后直接判断这个乘积与 ngcd

但是,如果某一时刻累积下来的样本的乘积为 0 了,为例不让样本丢失,我们直接退出循环进行判断即可。

每次计算的阈值可以倍增,并且加上一个上限,使用 128 是一种不错的选择(玄学)。


至此,你就可以通过 P4718 【模板】Pollard-Rho 了。代码

ll mul(ll a,ll b,ll m){ll z=(long double)a/m*b,res=(a*b-z*m)%m;return (res+m)%m;}
ll add(ll a,ll b,ll n){return (a+b>=n)?a+b-n:a+b;} 
ll qpow(ll a,ll b,ll m){
  ll res=1ll;
  while(b){
  	if(b&1) res=mul(res,a,m);
  	a=mul(a,a,m);
  	b>>=1;
  }
  return res;
}

const ll prm[12]={2,3,5,7,11,13,17,19,23,29,31};
bool miller_rabin(ll n){
  if(n<2) return false;
  for(int i=0;i<=10;i++){
  	if(n==prm[i]) return true;
  	if(n%prm[i]==0) return false;
  } 
  ll u=n-1,t=0;
  while(!(u&1)) u>>=1,t++;
  for(int i=0;i<=10;i++){
    ll a=prm[i],x1=qpow(a,u,n),x2;
    for(int j=1;j<=t;j++){
      x2=mul(x1,x1,n);
      if(x2==1&&x1!=1&&x1!=n-1) return false;
      x1=x2;
	}
	if(x1!=1) return false;
  }
  return true;
}

ll gcd(ll a,ll b){return (b==0)?a:gcd(b,a%b);}
ll f(ll a,ll c,ll n){return add(mul(a,a,n),c,n);} 
ll rad(ll n){return 1ll*rand()*rand()%n*rand()%n+1ll;}

ll pollard_rho(ll n){
  if(n==4) return 2;
  ll x=rad(n-1),c=rad(n-1),y=x;
  x=f(x,c,n);y=f(x,c,n);
  for(int lim=1;x!=y;lim=min(128,lim<<1)){
  	ll cur=1ll,nxt=0;
  	for(int i=1;i<lim;i++,cur=nxt){
  	  nxt=mul(cur,abs(add(x,n-y,n)),n);
  	  if(!nxt) break;//x=y	
	  x=f(x,c,n);y=f(f(y,c,n),c,n);
	}
	ll d=gcd(cur,n);
	if(d!=1) return d; 
  }
  return n;
}

void sol(ll n){
  ll d=pollard_rho(n);
  while(d==n) d=pollard_rho(n);
  if(miller_rabin(d)) ans=max(d,ans);
  else sol(d);
  if(miller_rabin(n/d)) ans=max(ans,n/d);
  else sol(n/d);
}

void wrk(){
  n=read();
  if(miller_rabin(n)){puts("Prime");return;}
  ans=0;sol(n);print(ans);
}

P4358 [CQOI2016] 密钥破解

一种非对称加密算法的密钥生成过程如下:

1.任选两个不同的质数p,q

2.计算N=p×qr=(p1)(q1)

3.选取小于r,且与r互质的整数e

4.计算整数d,使得ed1(modr)

5.二元组(N,e)称为公钥,二元组(N,d)称为私钥。

当需要加密消息n时,(假设n是一个小于N的整数,因为任何格式的消息都可转为整数表示),使用公钥(N,e),按照

nec(modN)

运算,可得到密文c

对密文c解密时,用私钥(N,d),按照

cdn(modN)

运算,可得到原文 n。算法正确性证明省略。

由于用公钥加密的密文仅能用对应的私钥解密,而不能用公钥解密,因此称为非对称加密算法。通常情况下,公钥由消息的接收方公开,而私钥由消息的接收方自己持有。这样任何发送消息的人都可以用公钥对消息加密,而只有消息的接收方自己能够解密消息。

现在,你的任务是寻找一种可行的方法来破解这种加密算法,即根据公钥破解出私钥,并据此解密密文。

e,N,c262,c<N

题意写得非常长,但是并不难。

发现只要我们找到 p,q,就做完了,然而这个 p,q 可以直接交给 Pollard_rho 即可。代码


P4714 「数学」约数个数和

给你一个正整数 N,请计算 N(所有约数的)×K 约数个数和。

答案可能很大,请输出对 998244353 取模的结果。

1N1018,0K1018

首先,对于 K=0 的情况,f0(x)=σ(x) 是积性函数。

而当 K>0 时,我们有

fi(n)=dnfi1(n)

这相当于一个狄利克雷卷积,而根据狄利克雷卷积卷积的性质,我们可以知道 fi(n) 也是积性函数。

于是现在问题就变成了如何求 fK(pq),因为我们把 n 分解之后就可以得到若干个 fK(pq) 的乘积。

发现这是简单的,等价于组合数学中在 q+1 个空位中插入 K+1 个板,可以重复。这时经典问题,你把每两个板之间就加一个球就可以得到

(q+K+1K+1)

根据对称公式,相当于

(q+K+1q)

q62,所以可以直接用下降幂暴力计算。

至于分解质因数,交给 Pollard_Rho 就可以了。代码


6 离散对数问题

离散对数问题就是解决模 p 意义下的 logab,也就等价于解离散对数方程

axb(modp)

这样的 x 就是 logabx 可能存在多个,也可能一个都不存在,我们希望找到这样的一个 x

ap 的时候,我们可以用大步小步算法解决,反之则可以用扩展大步小步算法解决。


6.1 大步小步算法(BSGS)

大步小步算法全称 Baby Step Giant Step,简称 BSGS,适用于 ap 的情形。


整体思路用到了 根号平衡 的思想,设块长为 M,那么最终的 x 一定可以被表示成 x=AMB,0B<M

于是这个方程就可以被表示成

aAMbaB(modp)

只要我们直接暴力枚举 AB,用哈希表(pb_ds)存一下是否出现过,就可以用 O(max(A,B)) 的时间复杂度之内计算出答案。

根据欧拉定理(稍后会讲),有解就一定在 [0,p1] 中有至少一个解,所以这里 0B<M,0ApM

那么设块长为 p 时,复杂度最优,为 O(p)


于是你就可以通过 P3846 [TJOI2007] 可爱的质数/【模板】BSGS 啦!代码

int BSGS(int a,int b,int H){
  int A=1,B=sqrt(H)+1;a%=H,b%=H;
  if(H==1||b==1) return 0;
  if(!a) return !b?1:-1;
  gp_hash_table<int,int> mp;
  for(int i=1;i<=B;i++) mp[1ll*(A=1ll*A*a%H)*b%H]=i;
  for(int i=1,AA=A;i<=B;i++,AA=1ll*AA*A%H)
    if(mp.find(AA)!=mp.end()) return i*B-mp[AA];
  return -1;
}

容易发现你从小到大枚举时就可以得到最小的非负整数解,而关于 x 的循环性质我们将会在后面阶与原根中讨论(循环的长度为 δa(p))。


在这里还有一些板题:


P3306 [SDOI2013] 随机数生成器

最近小 W 准备读一本新书,这本书一共有 p 页,页码范围为 0p1

小 W 很忙,所以每天只能读一页书。为了使事情有趣一些,他打算使用 NOI2012 上学习的线性同余法生成一个序列,来决定每天具体读哪一页。

我们用 xi 来表示通过这种方法生成出来的第 i 个数,也即小 W 第 i 天会读哪一页。这个方法需要设置 3 个参数 a,b,x1,满足 0a,b,x1<p,且 a,b,x1 都是整数。按照下面的公式生成出来一系列的整数:

xi+1a×xi+b(modp)

其中 mod 表示取余操作。

但是这种方法可能导致某两天读的页码一样。

小 W 要读这本书的第 t 页,所以他想知道最早在哪一天能读到第 t 页,或者指出他永远不会读到第 t 页。

1T50,0a,b,x1,t<p,2p109,p 是质数


考虑先全部左移一维,那么得到的数其实就是 x0,我们令 si=xiai,那么根据递推式,我们可以得到 aisi=aisi1+b,所以 si=si1+bais0=x0,则 si=x0+j=1ibaj

于是我们就可以将 xi 表示出来:

xi=aisi=aix0+bj=0i1aj=aix0+bai1a1=(x0+ba1)aiba1

这就可以用 BSGS 直接完成啦——注意特判 a=0,a=1 的情况。

总时间复杂度 O(Tp)代码


P4884 多少个 1?

给定整数 K 和质数 m,求最小的正整数 N,使得 111(N1)K(modm)

说人话:就是 1111111modm=K

6m10110<K<m,保证 m 是质数。


a 如果我们直接把 11111 分解出来是 100+101+102+,这是非常不好处理的。

我们考虑将这个数表示成一个数,也就是它乘 9 之后就会变成 10n19

那么此时这个方程就变成了

10n19K(modm)10n9K+1(modm)

于是这个东西就可以直接用 BSGS 完成了。

注意可能在乘的过程中爆 long long。代码


CF1106F Lunar New Year and a Recursive Sequence

有一串 n 个数的数列,给你 b1bk。当 i>k 时:

fi=(j=1kfijbj)mod998244353

已知 f1=f2==fk1=1,fn=m,问 fk 可能是多少。

k100n109

会用到原根相关知识,若不会原根建议先看后面原根部分。


有关于 的式子是非常不好求的,我们考虑把它转化成有关加法的式子从而可以通过矩阵快速幂来完成。

而这种需要让我们想到了 原根,众所周知,3998244353 的一个原根,我们记它为 G,并记 fn=Ggn,于是我们就把式子转化成了

Ggi=j=1kGgijbjmod998244353

把指数提下来,同时模数变成了 ϕ(p),我们就可以得到

gi=j=1kgijbjmod998244352

那么只要我们能求得 gi 那么求得 fi 也就是非常容易的了。

发现这个东西刚好是矩阵乘法的形式,也就是我们可以构造 k×k 矩阵

A=[000bk100bk1010bk2001b1]

从而得到

[00gk]Ank=[gnk+1gnk+2gn]

于是当我们知道 gk 时,可以快速算出 gn 的值。

而由于前面都是 0,所以实际上我们有

gk×Ank[k][k]gn(mod998244352)

其中 Ank[k][k] 是一个可以用矩阵快速幂处理出来的常数。

由于 998244352 并不是质数,所以当我们知道 gn 的时候 gk 可以直接用 exGCD 求出。


现在问题就转化成如何求出 gn,由于 Ggnfn(mod998244353),容易发现这时直接可以用 BSGS 求出的。

于是这道题就做完了,实现时注意时模 998244352 还是 998244353 即可。代码


6.2 扩展大步小步算法(exBSGS)

接下来我们来讨论更为普遍的问题 axb(modp),如果没有 ap 怎么处理?


从已知到未知: 由于 ap 的情形我们已经可以解决了,那么我们现在考虑把 a,p 从不互质凑到互质。

我们运用同余方程的运算法则,我们把两边同时除以 d=gcd(a,p) 就可以得到

adax1bd(modpd)

由于 adpd 一定是互质的,所以在模 pd 的意义下 ad 是又逆元的,于是我们可以把 ad 除过去,就是

ax1(bd)×(ad)1(modpd)

然而这时的 apd 并不一定互质,比如说 a=2,p=4 的情况,那么我们就重复上面的操作直到 ap,而这个操作只会最多进行 logp 次,这时容易证明的,在 a=2,p=2k 取到极值。

而注意到每除依次,x 就会 1。于是当操作 k 次后,我们需要计算其实是 xk。此时如果 b=0,直接返回 k 就可以了,这时需要特判的。

容易发现这样的时间复杂度是 O(p+log2p),后者有每一次 exgcd 求逆元的时间复杂度。


这样就可以通过 P4195 【模板】扩展 BSGS/exBSGSSP3105 MOD - Power Modulo Inverted 了。代码


int gcd(int a,int b){return b==0?a:gcd(b,a%b);}
void exgcd(int a,int b,int &x,int &y){
  if(b==0) return x=1,y=0,void();
  exgcd(b,a%b,y,x),y-=x*(a/b);
}
int inv(int a,int p){exgcd(a,p,x,y);return (x%p+p)%p;}

int BSGS(int a,int b,int H){
  int A=1,B=sqrt(H)+1;a%=H,b%=H;
  if(H==1||b==1) return 0;
  if(!a) return !b?1:-1;
  gp_hash_table<int,int> mp;
  for(int i=1;i<=B;i++) mp[1ll*(A=1ll*A*a%H)*b%H]=i;
  for(int i=1,AA=A;i<=B;i++,AA=1ll*AA*A%H)
    if(mp.find(AA)!=mp.end()) return i*B-mp[AA];
  return -1;
}

int exBSGS(int a,int b,int H){
  int d=gcd(a,H),cnt=0;a%=H,b%=H;
  while(d>1){
	if(b==1) return cnt;
	if(b%d) return -1;
	H/=d,b=1ll*(b/d)*inv(a/d,H)%H;
	d=gcd(H,a%=H),++cnt;
  }
  int res=BSGS(a,b,H);
  return ~res?res+cnt:-1;
}

P5345 【XR-1】快乐肥宅

给定 nki,ri,gi,求关于 x 的方程组

{k1xr1(modg1)k2xr2(modg2)knxrn(modgn)

(定义 00=1)在 [0,109] 范围内的最小整数解,或判断在这个范围内无解。

1n103,1ki,rigi107

难绷。


容易发现,我们的解题过程分成两部分:求解每一个方程的解求线性同余方程组的解


首先考虑第一部分,如何求解每一个方程的解呢?

根据 exBSGS 的求解过程可以知道,解的情况一定分成了三种:无解、唯一解和无穷解。

画出图来,解一定是一个 ρ 形的,也就是这个样子(from 小粉兔)

image

其中 ρ 的尾巴是我们依次 ÷gcd 的过程,这种是只有一个解;

而进入环上就变成了无穷解,而这个解是形如线性同余方程;

反之,没有出现过的就是无解。


尾巴的长度是可以直接求 ax1(modb) 的最小正整数解(这里和上面的板子会略有区别),这里的 a 是 exBSGS 化出来的值。那么我们就可以用一次 exBSGS 的过程求出第一个解的位置 a 和循环节的长度 b,这就转化成了方程 xa(modb)

反之对于只有一个解的情况,我们直接记它为答案,最后特判一下就可以了。


接下来考虑第二部分,求 n 个线性同余方程组的解。

由于不保证模数互质,所以这个需要用 exCRT 解决,而 103107 是容易爆 long long 的,如何判断呢?

考虑 exCRT 的过程,中间每一次我们都是 ans+=x*M,那么只要这次的 M>109,就不需要继续算后面的部分了,因为下一次一定就暴了。

所以这个时候我们只需要判断当前的解对于后面的方程是否都成立即可。


这样就做完了,时间复杂度为 n 次 exBSGS 的复杂度。具体实现中有细节。代码


7 数论函数基础

数论函数是数论中相当重要的一环,我们先来将一些基本的函数。

7.1 相关定义

  • 数论函数:定义域为正整数的函数称为 数论函数。因其在所有正整数处均有定义,故可视作数列。OI 中常见的数论函数的陪域(即可能的取值范围)为整数。

  • 加性函数:若对于任意 a,bN+ab 均有 f(ab)=f(a)+f(b),则称 f加性函数。注意区分代数中的加性函数。

  • 积性函数:若对于任意 a,bN+ab 均有 f(ab)=f(a)f(b),则称 f积性函数。容易发现 f(1)=1 是必要条件。

  • 完全积性函数:若对于任意 a,bN+ 均有 f(ab)=f(a)f(b),则称 f完全积性函数。完全积性函数一定是积性函数。

  • 数论函数的 加法:对于数论函数 f,gf+g 表示 fg 对应位置相加,即 (f+g)(x)=f(x)+g(x)

  • 数论函数的 数乘:对于数 c 和数论函数 fcf 表示 f 的各个位置乘 c,即 (cf)(x)=cf(x)。一般简记作 cf

  • 数论函数的 点乘:对于数论函数 f,gfg 表示 fg 对应位置相乘,即 (fg)(x)=f(x)g(x)。为与狄利克雷卷积符号 作区分,点乘符号通常不省略。


7.2 常见函数

n 的唯一分解为 i=1mpici,以下是一些常见的数论函数:

  • 单位函数:ϵ(n)=[n=1]完全积性函数

  • 常数函数:1(n)=1完全积性函数

  • 恒等函数:idk(n)=nkid1(n) 记作 id(n)完全积性函数

  • 除数函数:σk(n)=dndkσ0(n) 表示 n 的约数个数,记作 τ(n)d(n)σ1(n) 表示 n 的约约数和,记作 σ(n)σk(n) 有计算式

{i=1m(ci+1)k=0i=1mpi(ci+1)k1pi1k>0

根据乘法分配律,n 的所有约数的 k 次方之和可以写成

i=1mj=0cipijk

等比数列求和后即可。

  • 欧拉函数:φ(n)=i=1n[in],表示 n 以内与 n 互质的数的个数。后面我们会有所介绍。

  • 本质不同质因子个数函数:ω(n)=pP[pn],表示 n 的本质不同质因子个数。

  • 莫比乌斯函数:

μ(n)={1n=10d>1,d2n(1)w(n)otherwise

以上所有函数除 ω 是加性函数外,其余都是积性函数。根据计算式及积性函数的定义易证。


7.3 线性筛积性函数

在前面的素数部分,我们分析了线性筛素数,它提供了在线性时间内筛出具有特殊性质的积性函数在 1n 处所有取值的基本框架。

只要积性函数 f 可在 O(1) 时间内计算任意指数幂处的取值 f(pk),就适用线性筛。(这只是 f 可线性筛的 充分但不必要 条件。存在更弱的条件使得 f 可线性筛但并不常见,如 O(k) 计算 f(pk),这会在下一章介绍)。

根据积性函数的性质,只要预先求出 lowi 表示 i 的最小质因子 p 的最高次幂 pvp(i),对于 ipk,即可使用 f(lowi)f(ilowi) 计算 f(i)。其中 vp(i) 表示质因数次数符号。


下面放一个框架,from 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) 
  }
}

8 狄利克雷卷积

钢筋混凝土。


8.1 定义与性质

两个数论函数 f,g 的狄利克雷卷积为一个新的数论函数,记作 fg。定义

h(n)=x×y=nf(x)g(y)=dnf(d)g(nd)

上面简记成 h=fg。按照定义是计算狄利克雷卷积,复杂度为 O(nlnn)


狄利克雷卷积还有一些有趣的性质。

交换律,分配律,结合律

fg=gf(f+g)h=fh+gh(fg)h=f(gh)

这三个东西都可以用定义式稍微推一下就证明出来了。


单位元: ϵf=f


逆元:若函数 f,g 满足 fg=ϵ,则称 f,g 互为逆元。

积性函数总是有且仅有一个逆元。


f 可逆当且仅当 f(1)0

下面写出证明和逆元的计算式:

g=f1。当 f(1)=0 时,f(1)g(1)=1f(1)g(1)=ϵ(1)=1 矛盾。当 f(1)0 时,g(1)=1f(1),对于 n>1g(n),我们可以用已知的 g(1)g(n1) 推出

(fg)(n)=ϵ(n)dnf(d)g(nd)=0g(n)f(1)+dn,d1f(d)g(nd)=0g(n)=dn,d1f(d)g(nd)f(1)

这同时也说明了逆元的 唯一性


f=g 的充要条件是 fh=gh,其中 h(1)0

直接乘上 h 的逆元即可。


积性函数的狄利克雷卷积也是积性函数

设有两个积性函数 f,g,令 h=fg,设 a,b 满足 ab

h(ab)=dabf(d)g(abd)=Tab,da,tb,dt=Tf(dt)g(abdt)=datbf(d)f(t)g(ad)g(bt)=(daf(d)g(ad))(tbf(t)g(bt))=h(a)h(b)

第二行中,由于 ab,所以满足 da,tb,dt=Td,t 只有一对,可以从 w 中分别提取属于 a,b 的质因子得到。


积性函数的逆元也是积性函数

设有积性函数 f,它的逆是 g。设 a,b 满足 ab,证 g(ab)=g(a)g(b)

使用归纳法。当 ab1 时,根据计算式显然成立。假设我们对 a<a,b<b(a,b) 都已经完成了证明,根据求你计算式可得

g(ab)=dab,d1f(d)g(abd)=ia,jb,ij1f(i)f(j)g(ai)g(bj)=f(1)f(1)g(a)g(b)iaf(i)g(ai)jbf(j)g(bj)=f(1)f(1)g(a)g(b)ϵ(a)ϵ(b)=g(a)g(b)

综合后面的两个性质,两个积性函数的积与商都是积性函数,但和与差不是。


8.2 线性筛狄利克雷卷积

给出 f,g 如何快速计算 h=fg 呢?


我们直接用定义式计算,把枚举因数变成枚举倍数,根据调和级数,时间复杂度为 O(nlnn)


如果 f,g 是积性函数,我们可以尝试以下的优化,写出 h 的表达式,有

h(n)={1n=1c=0kf(pc)g(pkc)n=pkh(pk)h(m)n=pkm(m>1,pm)

对于第一项和第三项,我们都可以在线性筛中总代价 O(n) 求出,而现在问题就来到了第二项。

如果 f,g 在质数幂处的取值已经求出,则需要 O(k) 的时间计算,我们尝试估算它的复杂度。考虑所有 nk 的质数对复杂度产生 O(k) 的贡献,因此

T(n)=x=1log2nxπ(nx)=x=1log2nxnxlnnx=1lnnx=1log2nx2nx

x2nx 随着 n 增大,而 x2 的增大速度,远小于 nx 的减小速度,于是从这一点入手,我们考虑证明 x2nxO(n)

x=1 是显然成立,于是考虑 x[2,log2n]x2 的最大值 log22nnx 的最大值 n 之积,因为当 x+log2xx 的高阶无穷小,所以 O(nlog2n)O(n)

因此,

T(n)1lnni=1log2nO(n)=O(n)

综上,使用线性筛求出两个在质数幂处取值已知的积性函数的狄利克雷卷积在 1n 处的取值的时间复杂度是 O(n)

这样我们就得到了积性函数线性筛更弱的条件:可以 O(k) 时间计算质数幂处的取值。


P6222 「P6156 简单题」加强版

给出 k,多次给出 n,求

i=1nj=1n(i+j)Kμ2(gcd(i,j))gcd(i,j)

1n107,K231,T104

会用到稍后要讲的莫比乌斯反演。


枚举 gcd,再进行莫比乌斯反演

i=1nj=1n(i+j)Kμ2(gcd(i,j))gcd(i,j)=d=1nμ2(d)di=1nj=1n[gcd(i,j)=d](i+j)k=d=1nμ2(d)di=1ndj=1nd[gcd(i,j)=1]dk(i+j)k=d=1nμ2(d)di=1ndj=1nddk(i+j)kti,tjμ(t)=T=1n(dTμ2(d)dμ(Td))Tki=1nTj=1nT(i+j)k

我们需要线性筛预处理出 f=(d×μ2)μ,并对 Tk 做前缀和,整除分块求解,时间复杂度 O(nlogklogn)

这就需要用到我们上面讨论的线性筛狄利克雷卷积。代码


简单版本:P6156 简单题


8.3 狄利克雷前缀和

对于任意数论函数 f 卷常数函数 1 等价于对 f狄利克雷前缀和:令 g=f1,则 g(n)=dnf(d)

对每个 n 计算给定数论函数在其所有因数处的取值之和有很好的实际含义,因此狄利克雷前缀和是相当重要的。


首先容易想到一种暴力做法,直接枚举每一个数的倍数,这样就可以做到 O(nlnn)

但是并不是最优的,于是我们有了下面的算法。


将每个数 n 写成一个无穷的序列 an={c1,c2,},表示 n=pici,其中 pi 表示第 i 个质数。因为 xy 的充要条件是 ax(ci)ay(ci),所以 f1 可以看成对下标做关于其无穷序列的高维前缀和,即

g(n)=i,ad(ci)an(ci)f(d)

根据高位前缀和的求法,枚举每一维并将所有下标关于该维做前缀和,可以得到狄利克雷前缀和的实现方法:

初始令 xi=f(i),从小到大枚举每一个质数 pi,枚举 k,将 xpik 加上 xk,相当于 k 的贡献给到 ak(i) 加上 1 之后的下标。

最终得到的 x 即为 g。根据小于 n 的素数倒数之和是 lnlnn 这一结论(埃氏筛法),狄利克雷卷积的时间复杂度是 O(nlnlnn)

于是你就可以通过 P5495 【模板】Dirichlet 前缀和 了。代码

#include <bits/stdc++.h>
using namespace std;
#define uint unsigned int

const int N=2e7+5;
uint seed,ans,a[N];
int n,p[N>>3],cnt=0;
bool vis[N];

uint rd(){seed^=seed<<13,seed^=seed>>17,seed^=seed<<5;return seed;}

void init(){
  for(int i=2;i<N;i++){
    if(!vis[i]) p[++cnt]=i;
    for(int j=1;j<=cnt&&i*p[j]<N;j++){
      vis[i*p[j]]=true;
      if(i%p[j]==0) break;
    }
  }
}

int main(){
  cin>>n>>seed;init();
  for(int i=1;i<=n;i++) a[i]=rd();
  for(int i=1;i<=cnt;i++) for(int j=1;p[i]*j<=n;j++) a[p[i]*j]+=a[j];
  for(int i=1;i<=n;i++) ans^=a[i];
  cout<<ans;return 0;
}

9 整除分块

整除分块又称为数论分块,因其解决的问题与整除密切相关而得名。数论分块是用于求解形如

i=1nf(i)g(ni)

的合适,前提为 f 的前缀和可以快速计算。


9.1 介绍

如果 ni 的数量不多,那么我们可以考虑转化成贡献形式,把原式写成若干个 g(ni) 乘上一段 f 的和。那么不同的 ni 到底又多少个呢?


i[1,n],nN+,不同的 ni 至多只有 2n 个。

证明是简单的:

  • in 时,ni 只有 n 个。

  • i>n 时,nin,也只有 n 个。


根据这条结论,我们枚举 O(n) 种整除值 d,求出最小和最大的 i 使得 ni=d,分别记作 l,r,那么原式可以写成

dg(d)i=lrf(i)

只要我们能对 f 进行快速的前缀和预处理,在 g 处能快速得到值,那么整个和式就可以在 O(n) 的时间复杂度内求得。


那么问题就转化成求使得 ni=d 的最大和最小的 i


转化一下,发现 ni=di 有两条限制:

i(d+1)>n

idn

两边把 d 除过去就可以得到

nd+1<ind

那么我们不重不漏地枚举所有整除值时,可以对于每一个 i 计算 nilr,借助上面的式子跳过这个极长的连续段。

具体地,我们令当前枚举到的 il,此时整除值为 d=ni。因为使得 ni=d 的最大的 i 等于 nd,所以

r=nnl

g(d)(s(r)s(l1)) 算入答案,下一次 l=r+1 表示跳过了 lr 这一段,如果 l>n 就退出即可。(s 是前缀和)


这样每一次的整除值都可以被算一次,时间复杂度 O(n)

  • i 的上界不为 n 时,要特殊处理:若 mn,每次要把 rm 取最小值;反之要特殊处理 ni=0 的情况。

  • 写的时候不要写成了 rn 了!(有些人因为这个常常调很久)


9.2 拓展

拓展也是较为简单的。


我们尝试将 向下取整 转换成 向上取值

对于左边界 l,求出使得 nl=nr 的最大的 r

k=nl,那么

nr>k1r(k1)<nr<nk1rn1k1

最后一步是因为 n,k 都是正整数,于是我们每次有

r=n1nl1

注意到 nl=1 时上界为 +,所以要去实际上界。


另外一个拓展就是我们尝试进行 高维数论分块

当和式中出现若干个下取整,形如

i=1nf(i)j=1mg()

时,我们只需要每次令

r=minj=1c(njnjl)

即可。最后要对 nmin

时间复杂度 nj

因为我们把存在 nj 满足 njinji+1 的位置 i 视作断点,总断点数量为每个下取整式的断点数量相加,于是就有该时间复杂度。


9.3 例题

几道简单题。


CF1603C Extreme Extension

对于一个数列,定义一次操作为选择数列中任何一个元素 ai ,将这个位置替换成两个正整数 x,y 满足 x+y=ai

定义一个数列的极端值为将这个数列变成 不降 数列的最小操作次数

给定一个长度为 n 的数列 a1ai105 ),让你求它的所有非空子段的极端值之和,对 998244353 取模

一个测试点中有多组数据,保证所有数据 n 的规模总和不超过 105


P2260 [清华集训2012] 模积和

i=1nj=1m(nmodi)×(mmodj),ij

mod 19940417 的值

1n,m109


P3579 [POI2014] PAN-Solar Panels

对于 n 组询问,每组询问给定四个整数 a,b,c,d,从区间 [a,b][c,d] 中任意选取两个整数 xy,求 gcd(x,y) 的最大值是多少。

1n10001ab1091cd109


10 欧拉函数

欧拉函数是一类非常重要的数论函数。


10.1 定义与性质

欧拉函数的定义为在 [1,n] 中与 n 互质的整数的个数,记作 φ(n)

显然,当 n 是质数时,有 φ(n)=n1


p 为质数,则 φ(pk)=(p1)×pk1

[1,pk] 中不是 p 的倍数的数都和 pk 互质。因此 φ(pk)=pkpk1=(p1)pk1


φ积性函数。即若 φ(ab)=φ(a)×φ(b)

设与 a 互质的数为 {a1,a2,,aφ(a)},那么在 [1,ab] 内与 a 互质的整数可以表示为 i×a+aj(0i<b,1jφ(a))

因为 ab,所以 ia(0i<b) 在模 b 意义下互不相同,即对于每一个 aj{i×a+aj}(0i<b) 均构成 b 的完全剩余系 {0,1,,b1},即随着 i0 变到 b1i×a+ajb 的余数取遍 0b1

因此,对于每一个 aj,满足 bi×a+aji 的个数为 φ(b),所以与 ab 互质的数的个数为 φ(ab)=φ(a)+φ(b)。得证。


根据算术基本定理,设 n 被唯一分解为 i=1mpici,则 φ(n)=n×i=1mpi1pi

根据前面两个性质

φ(n)=pφ(pk)=ppk×p1p=n×i=1mpi1pi;

得证。

当然,我们还可以用 容斥原理 来证明。

考虑去掉所有被至少一个 n 的质因子整除的数,再加上被至少两个 n 的质因子整除的数,以此类推,我们可以得到

φ(n)=n×S(1)|S|pSp

其中 Sn 的质因子集合的子集。容易发现,这个式子就是

n×pi(11pi)

这样我们也可以反推到积性函数上面。设 a=i=1m1pici,b=i=1m2qidi。由于 ab,所以 pi,qi 互不相同。因此

ab=i=1m1pici×i=1m2qidi

于是有

φ(ab)=ab×i=1m1pi1pi×i=1m2qi1qi=φ(a)×φ(b)


ab,则 φ(ab)=a×φ(b)

根据前面 φ 的计算式,容易发现这时比较显然的。

于是我们也容易得到这样的结论:

ab,则 φ(a)φ(b)


欧拉反演:

dnφ(d)=n

考虑 φ(n) 的定义是与 n 互质的数的个数,那么容易发现 φ(n) 就是分母为 n 的最简基本分数的个数,而法里级数 Fn 既包含分母不超过 n[0,1] 范围内的所有最简分数,我们尝试从这一点来论证欧拉反演。

我们把法里级数中非零的部分提出来,例如当 n=12 时,在约分成最简基本分数前,分母为 12 的所有基本分数集合是

112,212,312,412,512,612,712,812,912,1012,1112,1212

约分得到

112,16,14,13,512,12,712,23,34,56,1112,11

我们可以根据它们的分母将这些分数分组,就可以得到

11;12;13,23;14,34;16,56;112,512,712,1112

对此,我们如何就是呢?

发现 12 的每一个因子都出现在分母上,与其一起出现的还有 φ(d) 个分子,从而

φ(1)+φ(2)+φ(3)+φ(4)+φ(6)+φ(12)=12

也就是

dnφ(d)=n

这就是 欧拉反演


同时它可以被表示成狄利克雷卷积的形式,也就是 φ1=id,这在杜教筛中颇有应用。


2φ(n)(n>2)

假设 xn,那么一定有 (nx)n。于是与 n 互质的数是两两配对的,唯一的特殊情况就是 x=nx 的时候。

容易发现这种情况显然是满足条件的,即 x=n2,那么 gcd(n,x)=x1。得证。这个性质在莫反的某道题中有所应用。


使得 gcd(n,x)=d1xnx 的个数为 φ(nd)

使得这个条件成立的条件可以被表示成 ndxd,同时 1xdnd,从而答案就是 φ(nd)。得证

如果我们把条件转移到 0x<n,也是成立的,因为我们之前定义了 gcd(0,n)=gcd(n,n)=n


10.2 线性筛欧拉函数

根据上面欧拉函数的计算式,我们可以得到它的线性筛法,若 pn,则

φ(n)={p×φ(np)p2n(p1)×φ(np)p2n

于是代码就是简单的

  phi[1]=1;
  for(int i=2;i<N;i++){
  	if(!vis[i]) p[++cnt]=i,phi[i]=i-1;
  	for(int j=1;j<=cnt&&i*p[j]<N;j++){
  	  vis[i*p[j]]=true;
	  if(i%p[j]==0){
	  	phi[i*p[j]]=phi[i]*p[j];
	  	break;
	  }	
	  else phi[i*p[j]]=phi[i]*(p[j]-1);
	}
  }

10.3 欧拉定理

欧拉发现费马的定理可以用下面的方式推广到非素数的模:

nφ(m)1(modm),nm

这和费马小定理是相当类似的,于是我们可以尝试用类似于费马小定理的方法来证明它。


φ(m) 个数 {knmodmkm0k<m} 就是 {kkm0k<m} 按照某种次序排列。

将它们相乘并用 0k<m,kmk 来除即可。


而当 m 是素数时,带入就可以得到费马小定理。


10.4 扩展欧拉定理

再把欧拉定理进行一个扩展,可以得到

ab={abmodφ(m)gcd(a,m)=1abgcd(a,m)1,b<φ(m)a(bmodφ(m))+φ(m)gcd(a,m)1,bφ(m)(modm)


感性理解:

gcd(a,m)1 时,不断乘 a 会让 aimgcd 不断变大,这到某个特定的 ar 之后 gcd 就不再变化,因为此时 gcd(ar,m) 受到 m 的每个与 a 相同的质因子的次数限制。

令这个 gcd=d,考虑 armodm,ar+1modm,,显然它们均是 d 的倍数,且除以 d 之后会取遍所有与 md 互质的数,这时因为 amd

根据欧拉定理,akmodmd 有循环节 φ(md),因此 (akmodmd)×dakmodm,也有循环节 φ(md),故从 ar 开始,ar+kmodm 有循环节 φ(p)

比较抽象,非常感性。


严谨证明 摘自 OI-wiki

  • a 的从 0 次,1 次到 b 次幂模 m 构成的序列中,存在 r,s,使得前 r 个数(即从 a0modmar1modm)互不相同,从 r 开始,每 s 个数就循环依次。

    在 3.5 中,我们对这个遗留问题进行了讨论。

  • a 为素数的情况,该是成立。

    证明

    • 若模 m 不能被 a 整除,那么有 gcd(a,m)=1 成立,根据欧拉定理容易证明。

    • 若模 m 能被 a 整除,那么存在 rm 使得 m=arm,且 gcd(a,m)=1 成立。所以根据欧拉定理有 aφ(m)1(modm)

    又由于 gcd(ar,m)=1,所以根据欧拉函数的计算式,容易得到 φ(m)=φ(m)×(a1)ar1,即我们有 φ(m)φ(m)

    所以 aφ(m)1(modm),φ(m)φ(m)aφ(m)1(modm),即 aφ(m)=km+1,两边同时乘 ar,得 ar+φ(m)=km+ar,因为 m=arm

    所以 m 中素因子 a 的次数 r 满足:arar+φ(m)(modm)。我们可以简单变换形式,得到 推论

    b>rabar+(br)modφ(m)(modm)

    又由于 m=arm,所以 φ(m)=φ(ar)φ(m)φ(ar)=ar1(a1)r

    所以因为 φ(m)r,故有:

    arar+φ(m)armodφ(m)+φ(m)(modm)

    所以

    abar+(br)modφ(m)armodφ(m)+φ(m)+(br)modφ(m)aφ(m)+bmodφ(m)(modm)

    ababmodφ(m)+φ(m)

  • a 为素数幂的情况,该式成立。

    证明

    不妨令 a=pk,是否依然有 r,arar+φ(m)(modm)

    答案是肯定的,由于第一点可知存在 s 使得 as1(modm),所以 plcm(s,k)1(modm),所以令 s=sgcd(s,k) 时,我们能有 psk1(modm)

    此时有关系 sssφ(m),且 r=rkrφ(m),由 r,sφ(m) 的关系,依然可以得到 ababmodφ(m)+φ(m)(modm)

  • a 为合数的情况,该是成立。

    证明:

    只证 a 拆成两个素数的幂的情况,大于两个的用数学归纳法可证。

    a=a1a2,其中 ai=piki,而 ai 的循环长度为 si

    slcm(s1,s2),由于 s1φ(m),s2φ(m),那么 lcm(s1,s2)φ(m),所以 sφ(m),r=max(riki)max(ri)φ(m)

    r,sφ(m) 的关系,依然可以得到 ababmodφ(m)+φ(m)(modm)

证毕。乐。


于是你就可以通过 P5091 【模板】扩展欧拉定理代码


10.5 例题

煎蛋题。


P2158 [SDOI2008] 仪仗队


11 莫比乌斯函数

尝试按照《具体数学》的顺序引入莫比乌斯反演。


11.1 引入

莫比乌斯函数 μ(m) 是根据 19 世纪的数学家奥古斯特·莫比乌斯命名的,他还发现了著名的莫比乌斯带,μ(m) 对所有整数 m1 由等式

dmμ(d)=[m=1]

来定义。

这个式子实际上是一个递归式,左边的 μ(m) 和某些满足 d<mμ(d) 的值组成的和式。我们有相继带入前面 m,从而得到前面的值:1,1,1,0,1,1,1,0,0,1,1,0,

在 1857 年,Richard Dedekind 和 Joseph Liouville 注意到了如下重要的 “反演原理”。

g(m)=dmf(d)f(m)=dmμ(d)g(md)

这就是我们熟知的 莫比乌斯反演


发现这个东西的证明是容易的:如果 g(m)=dmf(d),那么

dmμ(d)g(md)=dmμ(md)g(d)=dmμ(md)kdf(k)=kmdmkμ(mkd)f(k)=kmdmkμ(d)f(k)=km[mk=1]f(k)=f(m)

反之,如果 f(m)=dmμ(d)g(md),则

dmf(d)=dmkdμ(k)g(dk)=dmkdμ(dk)g(k)=kmdmkμ(d)g(k)=km[mk=1]g(k)=g(m)

于是我们在这里就证明了 莫比乌斯反演

而莫比乌斯反演还可以被表示成

g=f1f=gμ


现在回到最开始的那个式子,发现其实就是 μ1=ϵ,由于我们之前证明的 积性函数的逆元也是积性函数,可以知道 μ 必是积性函数。这样一来,只要我们计算出 μ(pk) 就能算出 μ(m)

m=pk 时,根据最初的式子,我们知道对于所有 k1,都有

μ(1)+μ(p)+μ(p2)++μ(pk)=0

由此推出

μ(p)=1;μ(pk)=0,k>1

于是,我们就推出了 μ 的定义式

μ(m)={1m=10m(1)kkmω(m)


如果我们把之前的欧拉反演视作 φ(m) 的递归式,就能应用莫比乌斯反演得到

φ(m)=dmμ(d)md

也就是 μid=φ,也对狄利克雷卷积逆元有了另一个方面的解释。


接下来我们尝试用容斥的角度来理解莫比乌斯函数。

g(n)=ndf(d),即 g(n)f 在所有 n 的倍数处的取值和。现在已知 g,要求 f(1)。则 f(1) 等于 f1 的倍数处的取值和,减去在质数处的取值和,但是多减去了在两个不同质数乘积处的取值和,一次要加上这些值,但是多加了在三个不同质数乘积处的取值和,以此类推。因此,若 nk 个不同质数的乘积,则 f(1) 会收到 g(n) 系数为 (1)k 的贡献,如下图。

image

就是对 N 作容斥,得到贡献系数 μ


11.2 线性筛莫比乌斯函数

根据定义是,线性筛莫比乌斯函数是非常容易的。

  mu[1]=1;
  for(int i=2;i<=maxn;i++){
  	if(!vis[i]) p[++cnt]=i,mu[i]=-1;
  	for(int j=1;j<=cnt&&i*p[j]<=maxn;j++){
  	  vis[i*p[j]]=true;
	  if(i%p[j]==0) break;
	  mu[i*p[j]]=-mu[i];	
	}
  }

11.3 常见技巧

对于 莫比乌斯反演,我们常常用式子

[gcd(i,j)=1]=dgcd(i,j)μ(d)

它把 gcd(i,j) 带入了 n,还将 i,j 互质的条件转化为枚举 gcd(i,j) 的约数 d,然后对 μ(d) 求和。在 i,j 同样需要枚举的时候,可以先枚举 d 并计算合法的 (i,j) 对数,这样 i,j 合法当且仅当 di,dj,就把 i,j 独立开了。

于是我们就可以这样解决下面的式子:

i=1nj=1m[gcd(i,j)=1]=i=1nj=1mdgcd(i,j)μ(d)=d=1min(n,m)μ(d)i=1nj=1m[didj]=d=1min(n,m)μ(d)ndmd

相当于对最大公约数为 d 的倍数进行了容斥:加上最大公约数为 1 的倍数的对数,减去最大公约数为 pi 的倍数的对数,加上最大公约数为 pipj(ij) 的倍数的对数,以此类推。得到每个 d 的贡献系数就是莫比乌斯函数。

而这个式子是非常容易用整除分块完成。


d(ij)=xiyj[xy] 考虑单个质因子 p,再用中国剩余定理和并。设 a=vp(i)i 中含有质因子 p 的数量,b=vp(j),则 vp(ij)=a+b。对于 ij 的约数 d,若 vp(d)a,则令其对应 vp(x)=vp(d),vp(y)=0,即所有都算在 x 上面;若 vp(d)>a,则令其对应 vp(x)=0,vp(y)=vp(d)a。容易发现这样会形成双射,因此可证得这一结论。

11.4 例题

接下来,我们来将一些例题,由于比较多,所以单独提出来了。部分例题会用到上面的常见技巧。

前面的一些例题中有些写得比较简略,有些可能是👻乐团中的子问题,如果看不懂可以在那道题题解中找一下,或者直接问我。


P2522 [HAOI2011] Problem b

对于给出的 n 个询问,每次求有多少个数对 (x,y),满足 axbcyd,且 gcd(x,y)=kgcd(x,y) 函数为 xy 的最大公约数。

1n,k5×1041ab5×1041cd5×104

首先可以对式子做一个二维差分,转换成求下界为 1 的式子

i=1nj=1m[gcd(i,j)=k]

我们把 i,j÷k 就可以得到

i=1nkj=1mk[gcd(i,j)=1]

这就是一个经典的莫反式子,直接作就可以得到

d=1min(nk,mk)μ(d)nkdmkd

于是就可以用整除分块在 O(n+Tn) 时间内完成。代码


子问题:P3455 [POI2007] ZAP-Queries


P2257 YY的GCD

给定 N,M,求 1xN1yMgcd(x,y) 为质数的 (x,y) 有多少对。

T=104N,M107

问题就是求

i=1nj=1m[gcd(i,j)=P]

我们用莫反相关知识对其进行推导可以得到

=pPi=1nj=1m[gcd(i,j)=p]=pPi=1npj=1mp[gcd(i,j)=1]=pPd=1min(np,mp)μ(d)npdmpd

于是我们令 T=pd,那么可以得到

T=1min(n,m)nTmTpTpPμ(Tp)

前面可以直接用整除分块完成,而后面的 pTpPμ(Tp) 乍一眼是不好完成的。

但细想发现这是可以和莫比乌斯函数一起在线性筛中解决的,所以我们就可以直接预处理出其前缀和,从而做到时间复杂度 O(n+Tn)代码


双倍经验:P2568 GCD


SP5971 LCMSUM - LCM Sum

T 次询问,每次询问给出 n,求

i=1nlcm(i,n)

1T3×105,1n106

首先我们考虑把 lcm 转化成 gcd,那么

i=1nlcm(i,n)=i=1ningcd(i,n)=ni=1nigcd(i,n)=ndn1di=1ni[gcd(i,n)=d]=ndn1di=1ndid[gcd(i,nd)=1]=ndni=1ndi[gcd(i,nd)=1]

我们设

F(n)=i=1ni[gcd(i,n)=1]

只要我们求出这个,就可以计算上面的式子了。


首先我们可以暴力用莫反计算,也就是

F(n)=i=1nididnμ(d)=dnμ(d)dnd(nd+1)2=n2dnμ(d)(nd+1)

我们发现后面可以被拆成 μ(id+1)=μid+μ1=φ+ϵ,于是我们就可以得到

n(φ(n)+ϵ(n))2


还有另外一种方法就是对与 n 互质的数进行分析,如果 xn 则一定有 (nx)n,那么我们把它两两配对,就有 nφ(n)2

而显然 F(1)=1


现在原式就变成了

ndnd(φ(d)+ϵ(d))2=n2(1+dndφ(d))

而这里的 dndφ(d)=1(id×φ) 是可以用线性筛筛出来的,于是总时间复杂度 O(T+n)代码


P4318 完全平方数

n 次询问,每次询问给出一个 k,查询第 k 个不是完全平方数的正整数倍数的数。

1k109,1n50

f(n) 表示 n 的非完全平方数倍数的数,那么我们最后只需要二分答案即可。

怎么求 f(n) 呢?

发现可以先去掉 4,9, 的倍数,而接下来就要加上 (p1p2)2 的倍数,这不难让我们想到莫比乌斯函数。

相当于做容斥,那么我们就有

f(n)=i=1μ(i)ni2

直接计算,时间复杂度 O(nlogn)代码


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

给定 n,m,求

i=1nj=1mlcm(i,j)

1n,m107

i=1nj=1mlcm(i,j)=i=1nj=1mijgcd(i,j)=d=1mindi=1ndj=1md[gcd(i,j)=1]ij

后面的式子就和 SP5971 一样了。直接做就是线性的。代码


P3704 [SDOI2017] 数字表格

Doris 刚刚学习了 fibonacci 数列。用 fi 表示数列的第 i 项,那么

f0=0,f1=1

fn=fn1+fn2,n2

Doris 用老师的超级计算机生成了一个 n×m 的表格,

i 行第 j 列的格子中的数是 fgcd(i,j),其中 gcd(i,j) 表示 i,j 的最大公约数。

Doris 的表格中共有 n×m 个数,她想知道这些数的乘积是多少。

答案对 109+7 取模。

1T1031n,m106

稍微推一下式子就是

i=1nj=1mfgcd(i,j)=d=1min(n,m)fdi=1nj=1m[gcd(i,j)=d]=d=1min(n,m)fdi=1ndj=1md[gcd(i,j)=1]=d=1min(n,m)fdi=1ndj=1mdti,tjμ(t)=d=1min(n,m)fdt=1min(nd,md)μ(t)ntdmtd=T=1min(n,m)(dTfdμ(Td))nTmT

预处理一下括号内的东西,整除分块之后可以做到单次 O(n)


P5221 Product

给定 n,求

i=1nj=1nlcm(i,j)gcd(i,j)(mod104857601)

1n106,时限 200ms

有些人喜欢在写完👻之后来写这道题。


话不多说,直接暴力推一下式子就是了。

i=1nj=1nlcm(i,j)gcd(i,j)=i=1nj=1nijgcd(i,j)2=(i=1ni)2n1(i=1nj=1mgcd(i,j))2

前面的直接阶乘处理一下,后面的我们枚举 gcd 就可以得到

i=1nj=1mgcd(i,j)=d=1ndi=1ndj=1nd[gcd(i,j)=1]=d=1ndt=1ndμ(t)ndt2

于是两次整除分块就可以线性完成了,代码相当好些。代码


P3911 最小公倍数之和

对于 a1,a2,,an,求

i=1nj=1nlcm(ai,aj)

1n,ai5×104

给定具体的数是不好处理的,所以我们考虑先令 ci=j=1n[aj=i],即 i 的出现次数,而 V 为值域。

那么

i=1nj=1nlcm(ai,aj)=i=1nj=1naiajgcd(ai,aj)=d=1Vi=1nj=1naiajd[gcd(ai,aj)=d]=d=1Vi=1Vj=1Vijcicjd[gcd(i,j)=d]=d=1Vdi=1Vdj=1Vdijcidcjd[gcd(i,j)=1]=d=1Vdi=1Vdj=1Vdijcidcjdkikjμ(k)=d=1Vdk=1Vdμ(k)k2i=1Vkdj=1Vkdijcikdcjkd

我们令 T=kd 就可以得到

=T=1VdTμ(d)d2Tdi=1VTj=1VTijciTcjT=T=1VT(dTμ(d)d)(i=1VTiciT)2

于是只要我们令 f(T)=dTμ(d)d,g(T)=i=1VTiciT,原式就变成

T=1VTf(T)g2(T)

其中 f 可以在线性筛中处理出来,而 g 可以采取暴力枚举倍数和狄利克雷后缀和做到 O(VlnV)O(VlnlnV)

这样就做完了,查询时可以直接线性完成。代码。(暴力枚举计算 g


双倍经验:AGC038C LCMs


CF1285F Classical?

n 个整数 a1,a2,,an,求 max1i<jn(lcm(ai,aj))

2n105,1 ai105

求完了和,我们现在来求 max


考虑加上一个 gcd(ai,aj)=1 的条件。于是我们从大到小枚举每一个数,如果当前加入一个数 x,存在一个 >x 的数 y 满足 gcd(x,y)=1,那么所有 xzy 中间的所有 z 都是没有用的,因为与他们互质的数比 x 小了。

容易发现这是可以用单调栈直接维护的,那么我们如何计算出是否有与 x 互质的数呢?

通过上面有关 μ 的容斥原理,我们设 cntd 表示 d 的倍数的出现次数,那么答案就是

dxμ(d)cntd

只要这个值不为 0,我们就可以一直弹栈找到与它互质的那个数。

时间复杂度 O(i=1nd(i)),也就是 O(VlogV)


而对于 gcd1lcm 如何计算?

我们可以枚举每一个 gcd 都这样做一次,这样的时间复杂度是 O(Vlog2V) 的,但是它并不优秀。

发现如果 gcd(x,y)1,那么对于 ax,by,一定存在一组 (a,b) 满足 gcd(a,b)=1,lcm(a,b)=lcm(x,y),于是我们把所有 ai 的因数都加到集合中,一起跑上面的算法,时间复杂度还是 O(VlogV)代码


P3327 [SDOI2015] 约数个数和

给定 n,m,求

i=1nj=1md(ij)

1T,n,m50000

在常见技巧中,我们列出了整个式子的转化。

于是式子就是

i=1nj=1md(ij)=i=1nj=1mxiyj[xy]=i=1nj=1mxiyj[gcd(x,y)=1]

于是直接对这个式子莫反就可以得到

d=1min(n,m)μ(d)x=1ndy=1mdnxdmyd

g(n)=i=1nni,这时可以用整除分块预处理出来的。

于是式子就是

d=1min(n,m)μ(d)g(nd)g(md)

直接整除分块可以做到时间复杂度 O(nn+Tn)代码


P4619 [SDOI2018] 旧试题

给定 A,B,C

(i=1Aj=1Bk=1Cd(ijk))mod(109+7)

1A,B,C105,1T10

更为厉害的约数个数和。


多了一个变量,但是按照之前的转化方式是类似的

d(ijk)=uivjwk[gcd(u,v)=1][gcd(v,w)=1][gcd(u,w)=1]

那么我们就可以对这个东西进行莫反了

i=1Aj=1Bk=1Cd(ijk)=i=1Aj=1Bk=1Cuivjwk[gcd(u,v)=1][gcd(v,w)=1][gcd(u,w)=1]=u=1Ai=1Auv=1Bj=1Bvw=1Ck=1Cw[gcd(u,v)=1][gcd(v,w)=1][gcd(u,w)=1]=i=1Aj=1Bk=1CAiBjCkuiujμ(u)vivkμ(v)wjwkμ(w)=u=1min(A,B)v=1min(A,C)w=1min(B,C)μ(u)μ(v)μ(w)lcm(u,v)iAilcm(u,w)jBjlcm(v,w)kCk

我们可以令 fa(i)=ijAj,fb(i)=ijBj,fc(i)=ijCj,那么这是可以直接枚举倍数在 O(nlnn) 的时间复杂度内预处理出来的,计算时就变成了

u=1min(A,B)v=1min(A,C)w=1min(B,C)μ(u)μ(v)μ(w)fa(lcm(u,v))fb(lcm(u,w))fc(lcm(v,w))

然后你发现这还是 O(n3) 的,莫反根本没有用!


真的没有用吗?这个式子在哪些地方是有值的呢?

容易发现我们首先需要保证 μ(u),μ(v),μ(w) 都不是 0,并且 lcm(u,v)A,lcm(u,w)B,lcm(v,w)C

那么也就是对于任意的两个点,它们可以成为一组就意味着 μ(u)0,μ(v)0,lcm(u,v)max(A,B,C),然后我们尝试把这样的二元组建立一条边,边权为 lcm(u,v)


这个做法看起来根本不可行,并且直接这样建边是 O(n2) 的。

但是细想一下,这个条件是相当严的,要求 u,v,gcd(u,v) 都不能出现平方因子。于是我们可以先枚举 gcd(u,v),再分别枚举 ugcd(u,v)vgcd(u,v) 在枚举的过程中判断 μ(gcd),μ(u),μ(v)0ugcd(u,v)vgcd(u,v)

每一次我们枚举了质数的子集,而 105 内只会出现 6 个不同的质数,所以这样枚举下来是 O(m) 的!

下面给出这一部分的代码实现。

  for(int g=1;g<=mx;g++)
    for(int i=1;i*g<=mx;i++)
	  if(mu[i*g])
	    for(int j=i+1;1ll*i*j*g<=mx;j++)
		  if(mu[j*g]&&gcd(i,j)==1){
            int u=i*g,v=j*g,w=i*j*g;
			/*...*/
		  }

进而,我们发现这样的点对并不多,实测发现只有 760741 个!赢麻了!


那么现在成了怎么找三元环,可以尝试阅读 这篇博客

也就是我们将无向边定向,每一条边从度数大的点连向度数小的,然后这样去枚举的时间复杂度就是 O(mm),具体可以看上面的链接。

于是我们就做完了!直接枚举就可以了。

  for(int x=1;x<=mx;x++){
    for(auto i:G[x]) vis[i.fi]=i.se;
    for(auto i:G[x]){
	  int y=i.fi,wxy=i.se;
      for(auto j:G[y]){
		if(!vis[j.fi]) continue;
        int z=j.fi,wyz=j.se,wxz=vis[z],val=mu[x]*mu[y]*mu[z];
		/*...*/
	  }
	}
	for(auto i:G[x]) vis[i.fi]=0;
  }

这样就可以做到 O(nlnn+mlnm),完全胜利!代码

存在把上面式子化成 O(n2logn) 的做法,简单来说就是只提出 u,对后面枚举的 i,j 都除以 u 去按照之前我们做过的套路完成,但此做法对后面的推导并无意义,具体可以去看第三篇题解。


三倍经验:CF236B Easy Number ChallengeCF235E Number Challenge


P5518 [MtOI2019] 👻乐团 / 莫比乌斯反演基础练习题

东风谷 早苗(Kochiya Sanae)非常喜欢幽灵乐团的演奏,她想对她们的演奏评分。

因为幽灵乐团有 3 个人,所以我们可以用 3 个正整数 A,B,C 来表示出乐团演奏的分数,她们的演奏分数可以表示为

i=1Aj=1Bk=1C(lcm(i,j)gcd(i,k))f(type)

因为音乐在不同的部分会有不同的听觉感受,所以 type 会在 {0,1,2} 中发生变化,其中:

f(0)=1f(1)=i×j×kf(2)=gcd(i,j,k)

因为乐团的歌实在太好听了,导致分数特别高,所以她们的分数要对给定的正整数 p 取模。

因为有很多歌曲要演奏,所以早苗给出了 T 组询问。

1A,B,C105    107p1.05×109    p{prime}    T=70

再来一个三重循环的题。

众所周知,cdqz 机房有且仅有三个👻,它们是:

  • image有名字 的👻,也是👻乐团的团长!

  • image没名字 的👻。

  • image:直接消失的👻,这样的👻常常从你身边飘过!

这三只👻就是 cdqz 机房的真神!/bx


题目名字都告诉你了,还是赶紧来推式子吧。

首先对于 Type = 0/1 是简单的,直接顺着思路往下面推就可以了,也就是这个样子:

对于 Type = 0,我们首先把 lcm 给处理掉再把四个量分离出来就是

i=1Aj=1Bk=1Clcm(i,j)gcd(i,k)=i=1Aj=1Bk=1Cijgcd(i,j)gcd(i,k)=(i=1Ai)BC(j=1Bj)AC1(i=1Aj=1Bgcd(i,j))C(i=1Ak=1Cgcd(i,k))B

前面两项是可以直接用阶乘完成的,也就是说我们只需要求

f(n,m)=i=1nj=1mgcd(i,j)=d=1min(n,m)di=1nj=1m[gcd(i,j)=d]=d=1min(n,m)di=1ndj=1md[gcd(i,j)=1]=d=1min(n,m)di=1ndj=1mdti,tjμ(t)=d=1min(n,m)dt=1min(nd,md)μ(t)ntdmtd

这个时候你可以把上面的指数部分单独提出来用一个函数完成,这样就可以做到线性,是可以过的,也就是后面代码1给出的(把三个 Type 分开了写的)。

但是你推到后面会发现这其实也是 Type = 2 的子问题,而这个式子是可以 O(n) 解决的,所以还是再推一步,这一步也是比较显然的,就是你把 t 提下来再用 T 代替 td

f(n,m)=d=1min(n,m)dt=1min(nd,md)μ(t)ntdmtd=d=1min(n,m)t=1min(n,m)dμ(t)ntdmtd=T=1min(n,m)(dTdμ(Td))nTmT

而中间那一块是可以直接用 O(nlogn) 的时间复杂度预处理出来的(直接暴力枚举),所以单次求就是 O(n)

恭喜你,获得了 20pts 的高分!代码


现在我们来研究 Type = 1,用同样的思路,写出来的式子也是简单的

i=1Aj=1Bk=1C(ijgcd(i,j)gcd(i,k))ijk=(i=1Aiij=1Bk=1Cjk)(j=1Bjji=1Ak=1Cik)1(i=1Aj=1Bgcd(i,j)ijk=1Ck)(i=1Ak=1Cgcd(i,k)ikj=1Bj)

把前面两项提出来就是形如

(i=1Aii)(j=1Bj)(k=1Ck)

预处理一下 i=1nii 可以直接用快速幂求出。

而对于后面两项,也就是求

i=1nj=1mgcd(i,j)ij

这个东西推导方式就较为类似了,笔者并没有把它推导 O(n),而是把指数单独拿出来处理了。

=d=1min(n,m)di=1nj=1m[gcd(i,j)=d]ij=d=1min(n,m)dd2i=1ndj=1md[gcd(i,j)=1]ij

于是可以把指数部分提出来,就变成了求

g(n,m)=i=1nj=1m[gcd(i,j)=1]ij=i=1nj=1mijdi,djμ(d)=d=1min(n,m)μ(d)d2nd(nd+1)2md(md+1)2

然后两次整除分块就可以完成。

很明显和上面 Type = 0 的优化类似,这时可以做到 O(n) 的,但是毕竟瓶颈也不在这里,式子也比较复杂,你直接这样作就好了。

预处理一下 i=1nii2i=1nμ(i)i2 即可,于是你就有 60pts 啦!代码


最后来搞 Type = 2 的情况,按照相同的思路先展开一下

i=1Aj=1Bk=1C(ijgcd(i,j)gcd(i,k))gcd(i,j,k)=(i=1Aij=1Bk=1Cgcd(i,j,k))(j=1Bji=1Ak=1Cgcd(i,j,k))1(i=1Aj=1Bgcd(i,j)k=1Cgcd(i,j,k))(i=1Ak=1Cgcd(i,k)j=1Bgcd(i,j,k))

对于前两项,其实就是(后面的 min 都代表 min(A,B,C)

i=1Aij=1Bk=1Cgcd(i,j,k)=d=1mini=1Ad(id)j=1Bdk=1Cd[gcd(i,j,k)=1]d=d=1mini=1Ad(id)dj=1Bdk=1Cdti,tj,tkμ(t)=d=1mint=1mini=1Atd(itd)dμ(t)BtdCtd

像往常一样,我们直接用 T 代替 td,而后面的 dTdμ(Td) 也就是 idμ=φ,我们就可以得到

=T=1mini=1AT(Ti)φ(T)BTCT

T 提出去就变成了

=(T=1minTφ(T)ATBTCT)(T=1mini=1ATiφ(T)BTCT)

容易发现这两项都可以用整除分块做出来的,稍微预处理一下 φ 的前缀和就可以了。

而对于后面两项,也就是求

i=1Aj=1Bgcd(i,j)k=1Cgcd(i,j,k)

按照之前的经验,我们尝试去枚举 gcd(i,j),令

g(x,n)=i=1ngcd(x,k)=dxφ(d)nd

然后就有

d=1mindg(d,c)i=1Aj=1B[gcd(i,j)=d]

你可以自己尝试推一下,就会发现只能用 O(nlogn) 的时间复杂度完成单次的查询,根本过不了!


那怎么办呢?

式子中还存在一个 gcd,那么我们就尝试去枚举 gcd(i,j,k) 于是式子就变成了:

i=1Aj=1Bgcd(i,j)k=1Cgcd(i,j,k)=d=1mini=1Adj=1Bd(dgcd(i,j))dk=1Cd[gcd(i,j,k)=1]=d=1mini=1Adj=1Bd(dgcd(i,j))dk=1Cdti,tj,tkμ(t)=d=1mint=1mini=1Atdj=1Btd(tdgcd(i,j))dμ(t)Ctd=T=1mini=1ATj=1BT(Tgcd(i,j))φ(T)CT=(T=1minTφ(T)ATBTCT)(T=1mini=1ATj=1BTgcd(i,j)φ(T)CT)

上面的内容和之前推第一部分类似,而现在我们发现其实中间的

i=1ATj=1BTgcd(i,j)

其实就是 Type = 0 中我们推的 f 函数,于是整个式子就变成了

(T=1minTφ(T)ATBTCT)(T=1min(T=1min(AT,BT)(dTdμ(Td))ATTBTT)φ(T)CT)

惊奇地发现这两部分前面的那一块其实是一样的,于是它们就抵消啦!

所以我们需要求的就是

T=1mini=1ATiφ(T)BTCT

T=1min(T=1min(AT,BT)(dTdμ(Td))ATTBTT)φ(T)CT

于是你就可以用两次整除分块线性求出来啦!


这样你就推出了本题的全部式子,写的时候注意细节,在指数上面的东西注意模的是 mod1 而不是 mod,整除分块不要手误了就行。

预处理出一些必要的量是根本不卡常的。代码1代码2


本题存在对于 Type = 2 的情况求 lnexp 回去的做法,具体可以去本题题解区。但笔者还是更喜欢莫反做法。


P3700 [CQOI2017] 小 Q 的表格

为了完成任务,小 Q 需要列一个表格,表格有无穷多行,无穷多列,行和列都从 1 开始标号。为了完成任务,表格里面每个格子都填了一个整数,为了方便描述,小 Q 把第 a 行第 b 列的整数记为 f(a,b)。为了完成任务,这个表格要满足一些条件:

  1. 对任意的正整数 a,b,都要满足 f(a,b)=f(b,a)
  2. 对任意的正整数 a,b,都要满足 b×f(a,a+b)=(a+b)×f(a,b)

为了完成任务,一开始表格里面的数很有规律,第 a 行第 b 列的数恰好等于 a×b,显然一开始是满足上述两个条件的。为了完成任务,小 Q 需要不断的修改表格里面的数,每当修改了一个格子的数之后,为了让表格继续满足上述两个条件,小 Q 还需要把这次修改能够波及到的全部格子里都改为恰当的数。由于某种神奇的力量驱使,已经确保了每一轮修改之后所有格子里的数仍然都是整数。为了完成任务,小 Q 还需要随时获取前 k 行前 k 列这个有限区域内所有数的和是多少,答案可能比较大,只需要算出答案 mod1,000,000,007 之后的结果。

1m1041a,b,kn4×1060x1018

建议盯着式子发呆。


首先,由于 f(a,b)=f(b,a) 所以我们只需要考虑半张表。

而你把第二个式子换一种写法就是

f(a,b)ab=f(a,a+b)a(a+b)

多写几个就变成了

f(a,b)ab=f(a,ka+b)a(ka+b)

那么换一种写法就是

f(a,b)ab=f(a,bmoda)a(bmoda)

我们发现这个东西和辗转相除求 gcd 是一样的,那么我们就可以得到这个很好的性质

f(a,b)ab=f(gcd(a,b),gcd(a,b))gcd(a,b)2

那么

f(a,b)=f(d,d)abd2

于是我们要求的答案其实就是

i=1kj=1kf(i,j)=d=1kf(d,d)i=1kj=1k[gcd(i,j)=d]abd2=d=1kf(d,d)i=1kdj=1kd[gcd(i,j)=1]ij

那么现在就是希望求

g(n)=i=1nj=1n[gcd(i,j)=1]ij

我们需要预处理出所有的 g(n) 才能在查询时以高效的方法完成。


但是按照上一道题我们推式子的方法,发现单次是 n 的,根本过不了!

这时我们考虑在 SP5971 中求的东西

i=1n[gcd(i,n)=1]i=n(φ(n)+ϵ(n))2

带进去就是

g(n)=i=1nj=1n[gcd(i,j)=1]ij=2i=1nj=1i[gcd(i,j)=1]ij1=2i=1nii(φ(i)+ϵ(i))21=i=1ni2φ(i)

于是这个东西就可以线性递推出来了,只要我们能够动态维护 f(d,d) 的区间和,那么最后整除分块一下就可以在 O(n) 的时间复杂度内求出来了。


但是我们发现上面所说的 f(d,d) 的区间和需要做到 O(1) 查询,由于 m104,所以我们不难想到用 O(n)O(1) 的分块来维护。

具体每个块每个元素维护块内的前缀和,在每一个块末尾维护块间的前缀和,修改时暴力修改即可。

总时间复杂度 O(mn),这道题为我们引入了一类式子新的推导方式。代码


12 奇妙筛法

已开工。


12.1 杜教筛

12.2 Min-25 筛

Min_25 筛可以在 O(n34logn) 的时间复杂度内解决一类 积性函数 前缀和问题。

说形象点就是 1s 能跑 1010,相当优秀。

要求:

  • f(p) 是关于 p 的低阶多项式,或者可以快速求出。
  • f(pc) 可以快速求出。

以下 p 代表质数,pi 表示第 i 小的质数,而 p0=1

同时我们钦定 lpf(i) 表示 i 的最小质因子。


首先,我们尝试对其进行推导,分成质数和合数两个部分

i=1nf(i)=1pnf(p)+1pen,1pnf(pe)(1inpe,lpf(i)>pf(i))

前面的部分是枚举素数,而后面的部分就是对于每一个合数枚举了其最小的质因子以及其次数。


考虑第一块的质数部分如何求值?

由于我们在前面要求了 f(p) 是关于 p 的低阶多项式,也就是

f(p)=iaipci

那么

1pnf(p)=iai1pnpci

现在问题就变成了求 1pnpk 了。

我们只对一个 k 拿出来讨论,如果多个就分别做以下的操作就可以了。


我们尝试用一个神秘的 dp,设

g(n,j)=i=1n[iPlpf(i)>pj]ik

(至于它有什么用,你先别急)

我们考虑 g(n,j) 是如何转移过来的。

不难想到用 g(n,j1) 进行转移,也就是减去了 lpf(i)=pj 的部分,它可以被表示成

pjkg(npj,j1)

由于 ik 是完全积性函数,我们不需要讨论前后是否互质!

而仔细一想,发现我们把质数多算了一次,它一共出现了 g(pj1,j1) 次,那么实际上减去的就是

pjk(g(npj,j1)g(pj1,j1))

其实后面的 g(pj1,j1) 就是前 j1 个质数的 k 次方和。

这样一来,我们就有了递推式

g(n,j)=g(n,j1)pjk(g(npj,j1)g(pj1,j1))

发现这是可以依次 dp 下去的,只要我们知道

i=2nik

就可以用质数处理出来。(但是时间复杂度根本过不去啊?你先别急)


回到引入 g 时的问题,它有什么用呢?

我们发现如果 px 是最大的 n 的质数,那么 g(n,x) 就是我们想要的答案。

所以如果通过 dp 推出 g(n,x) 的值就完全胜利,而我们把它简记成 g(n)。(具体细节你先别急)


假设我们现在已经可以快速算出 g 了,那原式会变成什么样子呢?

发现后面的那一块也是不好处理的,所以我们采用类似的 dp 方式,设 S(n,x) 表示 1nlpf(i)>px 的函数值和,容易发现答案就是 S(n,0),而我们有

S(n,x)=g(n)spx+pken,x<kf(pke)(S(npke,k)+[e>1])

其中 spx 表示前 x 个素数的答案,而容易发现当 px>n 是没有意义的,因为只会剩下一些大质数,所以我们的 pxn 级别的,完全可以预处理出来。

而后面的 [e>1] 则表示当前的 i=pe 的情况。

这样的递归是可以直接求的,时间复杂度为 O(n34logn) 的。具体证明可见 OI-wiki


现在我们回到对 g 的求解,具体怎么求?

发现直接处理出 1n 的所有值显然不太可能,n 太大了,但仔细观察两个式子,发现每次需要用到的就是

g(npj),g(npke)

这不就是类似于整除分块的东西么?

我们在第一章证明过

nab=nab

所以在整个过程中只会用到 1,2,,n,nn,,n 位置的值,就是整除分块的 ni 那些值。

在第九章我们知道个数是 2n 的,于是我们只需要对这 O(n)g 值进行 dp 预处理就可以了。


再来讲一讲具体的实现细节,我们用 id1[]id2[] 分别记录 ini>n 的值在 g 数组中的下标,而对于 i>n 的部分直接维护 ni 就可以了。

同时,由于 1 既不是质数也不是合数,所以在上面的推到中它都不应该被算进去,也就是 gS 都把 1 抠掉了,所以最后的答案是 S(n,0)+1


于是就可以通过 P5325 【模板】Min_25 筛 了。代码

const ll H=1e9+7;
int cnt=0,tot=0;
ll p[N],g1[N],g2[N],s1[N],s2[N],w[N],id1[N],id2[N],B,n,i6,i2;
bool vis[N];

ll qpow(ll a,ll b){
  ll res=1;
  while(b){if(b&1) res=res*a%H;a=a*a%H,b>>=1;}
  return res;
}

ll S1(ll x){x%=H;return x*(x+1)%H*i2%H;}
ll S2(ll x){x%=H;return x*(x+1)%H*(2*x+1)%H*i6%H;}
ll pos(ll x){return x<=B?id1[x]:id2[n/x];}

ll S(ll x,int y){
  if(x<=p[y]) return 0;
  int k=pos(x),res=((g2[k]-g1[k]-s2[y]+s1[y])%H+H)%H;
  for(int i=y+1;i<=cnt&&p[i]*p[i]<=x;i++)
    for(ll j=1,nw=p[i];nw<=x;++j,nw*=p[i]){
	  ll z=nw%H;
	  res=(res+(z-1)*z%H*(S(x/nw,i)+(j>1))%H)%H;
	}
  return res;
}

int main(){
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n,B=sqrt(n);
  i2=qpow(2,H-2),i6=qpow(6,H-2);

  for(int i=2;i<=B;i++){
	if(!vis[i]){
	  p[++cnt]=i;
	  s1[cnt]=(s1[cnt-1]+i)%H;
	  s2[cnt]=(s2[cnt-1]+1ll*i*i%H)%H;
	}
	for(int j=1;j<=cnt&&i*p[j]<=B;j++){
	  vis[i*p[j]]=true;
	  if(i%p[j]==0) break;
	}
  }

  for(ll l=1,r;l<=n;l=r+1){
	r=min(n/(n/l),n);
	w[++tot]=n/l;
	g1[tot]=S1(n/l)-1,g2[tot]=S2(n/l)-1;
	if(n/l<=B) id1[n/l]=tot;
	else id2[n/(n/l)]=tot;
  }

  for(int i=1;i<=cnt;i++){//g 数组的 dp,用了滚动数组
	for(int j=1;p[i]*p[i]<=w[j]&&j<=tot;j++){
	  int cur=pos(w[j]/p[i]);
	  g1[j]=(g1[j]-p[i]*(g1[cur]-s1[i-1])%H+H)%H;
	  g2[j]=(g2[j]-p[i]*p[i]%H*(g2[cur]-s2[i-1])%H+H)%H;
	}
  }
  cout<<(S(n,0)+1)%H;
  return 0;
}

现在我们换几个函数来筛,也就是我们最熟悉的 μφ

它们在质数处的取值也是相当容易的,μ(p)=1,φ(p)=p1,这样的低阶多项式是可以较为方便计算的。

直接把两个分开写出来就是这样的

const int N=1e6+5;
int cnt=0,tot=0;
ll p[N],w[N],id1[N],id2[N],n,B;
bool vis[N>>2];

int pos(ll x){return x<=B?id1[x]:id2[n/x];}

void Pr(int n){
  memset(vis,0,sizeof(vis));
  for(int i=2;i<=n;i++){
	if(!vis[i]) p[++cnt]=i;
	for(int j=1;j<=cnt&&i*p[j]<=n;j++){
	  vis[i*p[j]]=true;
	  if(i%p[j]==0) break;
	}
  }
}

namespace Mu{
  ll g[N];

  ll S(ll x,int y){
	if(x<=p[y]) return 0;
	ll res=-g[pos(x)]+y;
	for(int i=y+1;i<=cnt&&p[i]*p[i]<=x;i++)
	  res-=S(x/p[i],i);
	return res;
  }

  void SOL(){
	for(int i=1;i<=tot;i++) g[i]=w[i]-1;
    for(int i=1;i<=cnt;i++){
	  for(int j=1;p[i]*p[i]<=w[j]&&j<=tot;j++){
		int cur=pos(w[j]/p[i]);
		g[j]+=i-g[cur]-1;
	  }
	}
	cout<<(S(n,0)+1)<<'\n';
  }

}

namespace Phi{
  ll g1[N],g2[N],s[N];

  ll S(ll x,int y){
	if(x<=p[y]) return 0;
	int k=pos(x);
	ll res=g1[k]-g2[k]-(s[y]-y);
	for(int i=y+1;i<=cnt&&p[i]*p[i]<=x;i++){
	  for(ll j=1,nw=1;nw*p[i]<=x;++j,nw*=p[i])
		res+=nw*(p[i]-1)*(S(x/(nw*p[i]),i)+(j>1));
	}
	return res;
  }

  void SOL(){
    for(int i=1;i<=cnt;i++) s[i]=s[i-1]+p[i];
	for(int i=1;i<=tot;i++) g1[i]=1ll*w[i]*(w[i]+1)/2-1,g2[i]=w[i]-1;
    for(int i=1;i<=cnt;i++)
	  for(int j=1;p[i]*p[i]<=w[j]&&j<=tot;j++){
		int cur=pos(w[j]/p[i]);
		g1[j]-=p[i]*(g1[cur]-s[i-1]);
		g2[j]+=i-1-g2[cur];
	  }
	cout<<(S(n,0)+1)<<' ';
  }
}

void SOLVE(){
  cin>>n,B=sqrt(n),cnt=tot=0;
  memset(vis,0,sizeof(vis));
  Pr(B);
  for(ll l=1,r;l<=n;l=r+1){
	r=min(n,n/(n/l));
	w[++tot]=n/l;
	if(n/l<=B) id1[n/l]=tot;
	else id2[n/(n/l)]=tot;
  }
  Phi::SOL(),Mu::SOL();
}

int main(){
  /*2024.4.26 H_W_Y P4213 【模板】杜教筛 Min_25 筛*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  int _;cin>>_;
  while(_--) SOLVE();
  return 0;
}

但结果并不理想:(前两个是 Min_25,最下面的是杜教筛)

image

于是我们开始了常数优化。


优化也是简单的,你把预处理放在一起,就可以跑到 2.95s:

image

进而,把两个函数一起做(再把不需要 long long 的地方换成 int),于是就可以做到 1.44s!!!

image

代码

const int N=1e6+5;
int cnt=0,tot=0,p[N],n,B,w[N],id1[N],id2[N],s[N];
ll g1[N],g2[N];
bool vis[N>>2];

inline int pos(int x){return x<=B?id1[x]:id2[n/x];}

inline void Pr(int n){
  for(int i=2;i<=n;i++){
	if(!vis[i]) p[++cnt]=i,s[cnt]=s[cnt-1]+i;
	for(int j=1;j<=cnt&&i*p[j]<=n;j++){
	  vis[i*p[j]]=true;
	  if(i%p[j]==0) break;
	}
  }
}

inline pll S(int x,int y){
  if(x<=p[y]) return {0,0};
  int k=pos(x);
  pll res={g1[k]-g2[k]-(s[y]-y),-g2[k]+y};
  for(int i=y+1;i<=cnt&&p[i]*p[i]<=x;i++)
	for(int j=1,nw=1;1ll*nw*p[i]<=x;++j,nw*=p[i]){
	  pll cur=S(x/(nw*p[i]),i);
	  if(nw==1) res.se-=cur.se;
	  res.fi+=1ll*nw*(p[i]-1)*(cur.fi+(j>1));
    }  
  return res;
}

inline void SOLVE(){
  cin>>n,B=sqrt(n),tot=0;
  for(ll l=1,r;l<=n;l=r+1){
	r=n/(n/l);
	w[++tot]=n/l;
	g1[tot]=1ll*(n/l)*(n/l+1)/2-1,g2[tot]=n/l-1;
	if(n/l<=B) id1[n/l]=tot;
	else id2[n/(n/l)]=tot;
  }
  for(int i=1;i<=cnt&&p[i]<=B;i++)
	for(int j=1;p[i]*p[i]<=w[j]&&j<=tot;j++){
	  int cur=pos(w[j]/p[i]);
	  g1[j]-=1ll*p[i]*(g1[cur]-s[i-1]);
	  g2[j]+=i-1-g2[cur];
    }
  pll res=S(n,0);
  cout<<(res.fi+1)<<' '<<(res.se+1)<<'\n';
}

int main(){
  /*2024.4.26 H_W_Y P4213 【模板】杜教筛 Min_25 筛*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  int _;cin>>_;Pr(46340);
  while(_--) SOLVE();
  return 0;
}

12.3 Powerful Number 筛

12.4 扩展埃氏筛

13 阶与原根

n(n>1) 的简化剩余系在模 n 乘法意义下封闭且构成群。容易证明其满足封闭性和结合律,存在逆元和单位元。将群相关定义应用在其上,得到阶和原根(循环群和生成元)的概念。

                              ——Alex_Wei

13.1 阶

由欧拉定理可知,对于 aZ,mN,若 am,则有

aφ(m)1(modm)

因此满足同余式 an1(modm) 的最小正整数 n 存在,这个 n 称为 am 的阶,记作 δm(a)ordm(a)


阶有一些性质。

a,a2,,aδm(a)m 两两不同余。

这和我们在 3.5 里面探讨的遗留问题较为相似。

考虑反证,假设存在 ij,且 aiaj(modm),则有 a|ij|1(modp)

但是显然有 0<|ij|<δm(a),与阶的定义矛盾。得证。


an1(modm),则 δm(a)n

n=δm(a)q+r,0r<δm(a)

r>0,则

arar(aδm(a))qan1(modm)

与阶的定义矛盾(最小性)。得证。


mN,a,bZ,am,bm,则

δm(ab)=δm(a)δm(b)

的充分必要条件是

δm(a)δm(b)

  • 必要性:因为 aδm(a)1(modm)bδm(b)1 可知

    (ab)lcm(δm(a),δm(b))1(modm)

    由于上面的性质,我们知道

    δm(ab)lcm(δm(a),δm(b))

    所以我们有

    δm(a)δm(b)lcm(δm(a),δm(b))

    gcd(δm(a),δm(b))=1

  • 充分性:因为

    (ab)δm(ab)1(modm)

    那么

    1(ab)δm(ab)δm(b)aδm(ab)δm(b)(modm)

    所以

    δm(a)δm(ab)δm(b)

    又由于 gcd(δm(a),δm(b))=1

    δm(a)δm(ab)

    同理可得

    δm(b)δm(ab)

    所以

    δm(a)δm(b)δm(ab)

    而我们有

    (ab)δm(a)δm(b)(aδm(a))δm(b)×(bδm(b))δm(a)1(modm)

    δm(ab)δm(a)δm(b)

    所以

    δm(ab)=δm(a)δm(b)

得证。


kN,mN,aZ,gcd(a,m)=1,则

δm(ak)=δm(a)gcd(δm(a),k)

很重要的一个性质。

注意到

akδm(ak)=(ak)δm(ak)1(modm)δm(a)kδm(ak)δm(a)gcd(δm(a),k)δm(ak)

而由于 aδm(a)1(modm)

(ak)δm(a)gcd(δm(a),k)=(aδm(a))kgcd(δm(a),k)1(modm)

所以

δm(ak)δm(a)gcd(δm(a),k)

δm(ak)=δm(a)gcd(δm(a),k)

得证。


容易发现其实上面的证明方法都挺类似的。/kk


那如何求 δm(a) 呢?(其中 ma

  • 直接使用 BSGS 求 ax1(modm) 的最小正整数解。时间复杂度 O(m)

  • 根据阶的性质,对于 x=kδm(a),必然有 ax1(modm)

    因此,我们考虑先让 x=φ(m),然后对于 φ(m) 的每个质因子 p,用 x 不断试除 p 知道 x 无法整除 p 或者 aap1(modm)

    时间复杂度 O(m+log2m),因为需要分解质因数和求解欧拉函数,用 Pollard_rho 可以优化为 m14

    m 不大,则可以 O(m) 线性处理每个数最小质因子,单次查询即可 log2m


13.2 原根

原根——数论的神!


mN,gZ。若 gcd(g,m)=1,且 δm(g)=φ(m),则称 g 为模 m 的原根。

我们需要原根的原因是它可以 生成m 的缩系,即它的若干次幂取遍了与 m 互质的数。这使得我们在考虑解与模数互质且模数存在原根的同余方程时可以用原根的若干次幂代替未知数。这一点在求解高次剩余时非常有用。


原根判定定理:设 m3,gcd(g,m)=1,则 g 是模 m 的原根的充要条件是,对于 φ(m) 的每个素因子 p,都有 gφ(m)p1(modm)

这和我们上面讲的求阶的方法非常像。

  • 必要性:显然。

  • 充分性:因为 φ(m)p 的所有因子取遍了 φ(m) 除了其本身的所有因子,所以若存在 ad1(modm),使得 d<φ(m),则必然存在 φ(m) 的质因子 p 使得 dφ(m)p,这就说明存在 aφ(m)p1(modm),与假设矛盾。得证


原根个数:若一个数 m 有原根,则它原根的个数为 φ(φ(m))

m 有原根 g,则:

δm(gk)=δm(g)gcd(δm(g),k)=φ(m)gcd(φ(m),k)

所以如果 gcd(φ(m),k)=1,则有 δm(gk)=φ(m),也就是 gk 也是模 m 的原根。

而满足 gcd(φ(m),k)=11kφ(m)kφ(φ(m)) 个。得证。


原根存在定理:一个数存在原根当且仅当 m=2,4,pa,2pa,其中 p 是奇素数,aN

可以当一个结论背住,具体证明比较复杂,可见 OI-wiki


最小原根的范围估计

在 1959 年,中国数学家王元证明了最小原根的级别为 O(m0.25+ϵ)。结合原根存在定理和原根判定定理,我们得以在 O(m) 预处理每个数的最小质因子后单次 O(m0.25ω(m)logm) 求出 m 的最小原根,从而求出 m 的所有原根。


关于原根,还有一个有趣且重要的性质:

m 存在原根 g,则 n=φ(m),则 m 的简化剩余系与 n 次单位根同构。

这是因为对于任何一个 am,其均可以写成 gk 的形式。而任何一个 n 次单位根 ωk 也可以写成 ωnk 的形式。

同时,gk1(modp),且 ωnn=1。因此,我们可以将单位圆应用在 m 的简化剩余系上。


这也是为什么对于特定模数,我们可以使用原根代替 n 次单位根进行 FFT,所以当需要使用 2k 次单位根时,若 k 不超过 φ(p) 当中质因子 2 的个数且 p 存在原根,即可用原根代替 2k 次单位根。

于是就得到了 NTT。


13.3 例题

放两道简单题。

P5605 小 A 与两位神仙

这个游戏的规则是这样的:两个神仙先选定一个正整数 m,保证 m 是一个奇质数 p 的正整数次幂。然后进行 n 轮游戏,每轮中造梦者选定一个正整数 x,杰瑞米选定一个正整数 y,保证 (x,m)=1,(y,m)=1,即 xm 互质,ym 互质,接下来询问小 A 是否存在非负整数 a 使得 xay(modm)

神仙们说小 A 只有在每一轮游戏中都回答正确才能回到正常的生活中,不得已之下他只好求助于聪明的你。

1n2×1043m10181x,y<m

注意到模数 p 是奇素数的幂,所以存在原根 g


我们尝试将 xy 表示成 gXgY 的形式。


14 高次剩余问题

15 卢卡斯定理



后记

参考文献:

OI-Note Chapter4.1 整除与同余

OI-Note Chapter4.2 数论函数与求和

初等数论学习笔记 I:同余相关

初等数论学习笔记 II:分解质因数

初等数论学习笔记 III:数论函数与筛法

Pollard_Rho 算法 - cjTQX - 博客园 (cnblogs.com)

《具体数学》第四章

本文作者:H_W_Y

本文链接:https://www.cnblogs.com/H-W-Y/p/18140410/NumberTheory

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   H_W_Y  阅读(318)  评论(2编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起