一. 素数相关
0. 约定
若无特殊说明,本部分做以下记号规定。
p∈P,P 为质数集。
π(n) 表示 1 至 n 内的素数个数。
P+(n),P−(n) 分别表示 n 的最大/最小质因子。
νi 表示 i 的可重质因子个数。
1. 素数定理
π(x)∼xlnx(x→+∞)
∃0<a<b,s.t.axlnx<π(x)<bxlnx
常用于估算素数数组大小。
2. 筛法
埃氏筛不展开。
欧拉筛保证合数只被筛一次,复杂度 O(n)。即线性筛。
同时通过欧拉筛容易实现对于欧拉函数,莫比乌斯函数的预处理。
3. 判断素数
- 暴力。
暴力的 O(√n) 是一种准确素性测试算法的复杂度。
- Miller-Rabin 算法
Miller-Rabin 使用费马小定理和二次探测定理进行素性测试。
显然根据费小和二次探测有
ap−1≡1(modp)
∀x2≡1(modp),x≡±1(modp)
设需要检验素性的数为 x。取 y<x,那若 x 为质数则 yx−1≡1(modx)。
再令 x−1=t×2k。
根据二次探测定理,考虑对 yt×2k 不断开方,有
∃0≤k′<k,yt×2k′≡−1(modx)
或 yt≡1(modx)。
我们如此地对于 x 进行 m 次测试,复杂度是 O(mlog3x)。但当然这是个概率算法。
一组常用的底数是 {2,3,7,61,24251}。在 1016 内仅 46856248255981 发生误判。
#define int long long
int base[15]={0,2,3,7,61,24251};
inline bool Miller_Rabin(int n){
if(n<2||n==46856248255981ll)return false;
int t=n-1,k=0;
while(t%2==0){
t>>=1;
k++;
}
for(int i=1;i<=5&&base[i]<n;i++){
int x=qpow(base[i],t);
if(x==1)continue;
for(int j=1;j<k;j++)if(x!=n-1)x=(x*x)%n;
if(x!=n-1)return false;
}
return true;
}
4. Meissel-Lehmer 算法求 π(n)
以下令 pi 表示第 i 个素数。p1=2。
定义 ψi(n)=∑1≤x<n[P−(x)>pi],τi(n,k)=∑1≤x<n([P−(x)>pi]×[νx=k])。
由于容斥显然有
ψi(n)=∑k≥0τi(n,k)[pki≤n]
其中钦定 τi(n,0)=1。
考虑对于 n13≤m≤n12,其中 m=pω。
那么对于 k≥3 时有 τω(n,1)=π(n)−ω,τω(n,k)=0。
∴π(n)=τω(n,1)+ω=ψω(n)−τω(n,2)−τω(n,0)+ω
那么对于 π(n) 我们只需计算 ψω(n) 和 τω(n,2) 即可。
- 计算 τω(n,2)
考虑它的实际意义:即质数对 (p,q) 满足 m<p≤q 且 pq≤n 的个数。
显然 p∈[m+1,√n],那么亦有 q∈[p,np],可得:
τω(n,2)=∑p∈[m+1,√n](π(np)−π(p−1))
此时线性筛预处理容易做到 O(nm)。
- 计算 ψω(n)
通过容斥有
ψω(n)=ψω−1(n)−ψω−1(npω)
边界 ψ0(n)=[n]。
那么考虑 dfs 计算此式,此时复杂度难以接受。考虑对于边界加入 x≤m 的剪枝。
ψω(n)=∑x≤mμ(x)[nx]+∑xP−(x)≤m<xμ(x)ψπ(P−(x))−1(nx)
令
R=∑x≤mμ(x)[nx]
T=∑xP−(x)≤m<xμ(x)ψπ(P−(x))−1(nx)
∴ψω(n)=R+T
显然 R 容易预处理。
对于
T=−∑p≤m∑x≤m<xp[P−(x)>p]μ(x)ψπ(p)−1(nxp)
考虑分块计算,令
T1=−∑n13<p≤m∑x≤m<xp[P−(x)>p]μ(x)ψπ(p)−1(nxp)
T2=−∑n14<p≤n13∑x≤m<xp[P−(x)>p]μ(x)ψπ(p)−1(nxp)
T3=−∑p≤n14∑x≤m<xp[P−(x)>p]μ(x)ψπ(p)−1(nxp)
∴T=T1+T2+T3
首先 T3 显然通过树状数组枚举 p 统计对 ψ 的贡献在 O(nmlogn)
简单实现。
对于 T1,T2 由于 n14<p<P−(x),x≤m≤√n,故必有 x∈P。
∴T1=−∑n13<p≤m∑p<q≤mψπ(p)−1(npq)
∴T2=−∑n14<p≤n13∑p<q≤mψπ(p)−1(npq)
考虑计算 T1,T2。
T1 由于 npq<n13<p 故 ψπ(p)−1(npq)=1,所以 T1 容易 O(m) 计算:
T1=−∑n13<p≤m(π(m)−π(p))
对于 T2 再次拆开为 S1+S2=T2:
S1=∑n14<p≤n13∑p<q≤m[q≤np2]ψπ(p)−1(npq)
S2=∑n14<p≤n13∑p<q≤m[q>np2]ψπ(p)−1(npq)
对于 S1 由于有 p≤npq<√n<p2,故有 ψπ(p)−1(npq)=π(npq)−π(p)+1
。
然后你再分为 S1=Q1+Q2,
Q1=∑n14<p≤n13(1−π(p))∑p<q≤m[q≤np2]
Q2=∑n14<p≤n13∑p<q≤m[q≤np2]π(npq)
我们发现 Q1 简单处理即可,对于 Q2 进行类数论分块只有 π(np2) 块,所以保证 O(n23logn)。
对于 S2,有 √nm≤√nq<p 且 npq<p 故 ψπ(p)−1(npq)=1。
∴S2=∑√nm<p≤n13(π(m)−π(min{p,np2}))
复杂度 O(m)。
考虑整个过程的时间复杂度计算。令 m=O(n13log2n) 时复杂度最优为 O(n23logn)。
若将 Q2 分为五个部分计算(参见oi-wiki),可优化至 O(n23log2n)。
常数极大的代码。比最优解慢了近40s。
inline void init(){
Getprime();
sz[0]=1;
for(int i=0;i<S;i++)phi[i][0]=i;
for(int i=1;i<=2;i++){
sz[i]=p[i]*sz[i-1];
for(int j=1;j<S;j++)phi[j][i]=phi[j][i-1]-phi[j/p[i]][i-1];
}
return;
}
inline int Phi(int x,int id){
int P=p[id],r=sz[id];
if(!id)return x;
if(id<=2)return phi[x%r][id]+(x/r)*phi[r][id];
if(x<=P*P)return pi[x]-id+1;
if(x<=P*P*P&&x<N){
int y=sqrt2(x),t=pi[y],res=pi[x]-((t-id+1)*(t+id-2)>>1);
for(int i=id+1;i<=t;i++)res+=pi[x/p[i]];
return res;
}
return Phi(x,id-1)-Phi(x/p[id],id-1);
}
inline int Pi(int x){
if(x<N)return pi[x];
int X=Pi(sqrt2(x)),Y=Pi(sqrt2(sqrt2(x))),Z=Pi(sqrt3(x)),res=Phi(x,Y)+((X+Y-2)*(X-Y+1)>>1);
for(int i=Y+1;i<=X;i++){
int t=x/p[i];res-=Pi(t);
if(i>Z)continue;
int O=Pi(sqrt2(t));
for(int j=i;j<=O;j++)res-=Pi(t/p[j])-j+1;
}
return res;
}
5. Pollard-Rho 算法计算非平凡因子
对于寻找合数 n 的一种非平凡因子的朴素做法显然可以枚举 [1,√n] 来做到。当然你还可以通过预处理一点打表来达到 O(√nlnn)。
生日悖论是一个著名的问题。
钦定有 n 个人,一年 m=365 天。
那么 n 个人中没有任意两人生日相同的概率为
P=n∏i=1m−i+1m
那么反之存在二人生日相同即为 1−P。
∵1+x≤ex
∴P≤n∏i=1e−i−1m=e−n(n−1)2m
而著名的悖论在于带入 n=23 即有 P≤50%。由于反直觉被认为是悖论。
事实上在 n=√mln2 左右时有 P=50%。
考虑一个随机序列 X 由 f(x)=x2+c(modn) 生成。c 随机。
那么这个序列 X 在出现 xi=xj,i<j 时,X 就从 i 开始陷入循环。

即如上图所示,因其形像 ρ 而成为 rho。
那么这样的“伪随机”序列 X 产生环的期望其上述的生日悖论。
设 m 为 n 的最小非平凡因子。显然 m≤√n,再记 yi=xi(modm)。
∴yi+1=xi+1(modm)=x2i+c(modm)=(ximodm)2+c(modm)=y2i+c(modm)
那么对于新的序列 Y,若存在 i,j 使 xi≠xj 且 yi=yj,即 m∣|xi−xj|,n∤|xi−xj|.
那么令 g=gcd(|xi−xj|,n),g 必然是 n 的一个非平凡因子。
期望枚举 O(√m) 次来获得 g。而 m≤√n,故期望 O(n14)。
考虑优化。
- Floyd 判环
即通过双指针 i,j,其中 i 的速度是 j 的两倍,以此去遍历环,那么 i,j 相遇时是环长整数倍。当然还是检查 g=gcd(|xi−xj|,n)。复杂度 O(4√nlogn)。这里不做赘述。
- 倍增优化
多次求 gcd 是时间复杂度的瓶颈,考虑如何优化。
每次将 |xi−xj| 相乘,若对于一对 xI,xJ,有 g 为 m 的非平凡因子,那么设 S=∏|xi−xj|,必然有 g′=gcd(S,n) 亦为 n 的非平凡因子。
那么考虑累计到一定样本再进行 gcd。考虑将阈值设为倍增,每次判断 g′<n 即可。
6. 反素数
素数是因子只有两个的数,反素数就是因子最多的数而不是emirp,并且因子个数相同的时候值最小。显然反素数是相对于一个集合的。
考虑对于一个反素数 n=m∏i=1pkii,即有 m∏i=1(ki+1) 取 max。
考虑在给定范围内如何求反素数。
显然直接分解质因数无法接受。考虑反素数的性质。
- 反素数是从 2 开始的连续素数的幂次形式乘积。
显然。所以考虑 minm 使 m+1∏i=1>n,我们只需枚举前 m 个素数。
- 数值小的素数的幂次大于等于数值大的素数,即 k1≥k2≥⋯≥km。
还是显然。若有 ki<kj,i<j 则将 kj−ki 移至 pi 幂次上更优。那么最坏枚举幂次枚举至 INT_MAX
差不多 64 即可。
整个过程就容易用搜索实现。
CF27E。模板题。
inline void dfs(int x,int k,int m,int dep){
if(dep>n||x<=0ll||x>=ans||k>16)return;
if(dep==n){
ans=x;
return;
}
x*=p[k];
for(int i=1;i<=m;i++,x*=p[k])dfs(x,k+1,i,dep*(i+1));
return;
}
二. gcd 相关
1. 求 gcd
显然通过欧几里得/辗转相除容易在 O(logn) 时间内递归求出 gcd。
inline int gcd(int x,int y){
if(!y)return x;
return gcd(y,x%y);
}
考虑一个问题:求一组可行解 (x,y) 满足 ax+by=gcd(a,b)
设 ax1+by1=gcd(a,b),bx2+(amodb)y2=gcd(b,amodb)=gcd(a,b)。
∴ax1+by1=bx2+(amodb)y2=bx2+(a−b⌊ab⌋)y2
故 x1=y2,y1=x2−⌊ab⌋y2
那么剩余的按照普通的欧几里得递归做就行。
inline int exgcd(int a,int b,int &x,int &y){
if(!b){
x=1,y=0;
return a;
}
int res=exgcd(b,a%b,x,y),X=x;
x=y,y=X-(a/b)*y;
return res;
}
2. 裴蜀定理
∀a,b∈Z(a or b≠0),∃x,y∈Z,ax+by=gcd(a,b)。
证明:
设 g=gcd(a,b),s=min(ax+by)∈N+,t=⌊as⌋,显然 ∀x,y∈Z 有 g|ax+by。
∴amods=a−t(ax+by)=a(1−xt)+b(−yt)
那么 amods 是 a,b 的线性组合。同时有 0≤amods<s。再由 s 定义易知 amods=0。同理亦有 s|b。故 s 为 a,b 公约数。故 g≥s。
又因为 g|a,g|b 且 s 为 a,b 线性组合。显然 g|s。那么即得 g=s。
裴蜀定理的一个推论:a,b 互质的充要条件是存在 x,y∈Z 使 ax+by=1。此不做赘述。
那么这可看做是上述 exgcd 的一个补充。
3. 类欧几里得
虽然我并不知道这放在这个专题是否合适
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
2022-01-17 【做题记录】Ynoi2018 天降之物
2022-01-17 【学术】连分数
2022-01-17 【做题记录】Ynoi2015 盼君勿忘
2022-01-17 【做题记录】BJOI2016 水晶
2022-01-17 【做题记录】P4965 薇尔莉特的打字机
2022-01-17 【做题记录】POI2011 Lightning Conductor
2022-01-17 【做题记录】CF961G Partitions