Number Theory(2)
5 素数
接下来我们来讨论素数。
5.1 素数的例子
如果一个正整数
而其他那些还有非平凡因子的数都称为 合数,每一个大于
任何正整数
而 算数基本定理 表示:仅有一种方法将
关于素数的其他定理:
-
第
个素数 大约是 的自然对数的 倍: -
对于不超过
的素数个数 :
证明过于复杂,这里不给出证明,大家可以自行了解。
还有一种与素数相关的数:梅森数。
形如
的数,其中
如果
但是当
5.2 互素关系
当
我们写成
而一个分数是最简分数,当且仅当
那我们有没有什么好的方法来构造满足
答案是肯定的,他被成为 Stern-Brocot 树,其思想是从两个分数
- 在两个相邻的分数
和 之间插入 。
新的分数就是
接下来就是
于是我们把这个分数阵列可以看成是一棵无线的二叉树构造,就是这样
每一个分数都是
为什么这种构造是对的呢?为什么每次的
因为我们发现,在这个构造中任意一个阶段的相邻分数都满足
首先这个关系在
括号展开就一样了,而这也让我们发现
一个中位分数不一定在原先两个的中间,但它的确位于它们两个中间的某个地方,于是我们就不可能得到相同的分数。
但是我们现在仍然存在一个问题——会遗漏么?
这个问题时简单的,由于它是无限的,所以我们会不断用上面的单调性去逼近它,这样就不会漏。
这里我们再引入一个概念,阶为
容易发现它是 S-B 树 的子树,好像也没什么好讲的。
事实上,我们可以把 S-B 树 看成一个表示有理数的 数系,因为每一个正的最简分数都恰好出现一次。
我们用字母
我们记当前的字符串为
例如
这个东西和矩阵乘法的构造是比较像的,也就是我们尝试去构造矩阵
那么我们知道
然后我们就发现其实
于是我们就可以通过一个
还有一种方法就是改变
因为
类似的性质对
这样的定位就可以不用矩阵乘法。
关于互素的讨论到此结束,更多可以参见《具体数学》!
P8058 [BalkanOI2003] Farey 序列
把所有分子和分母都
的 最简真分数 从小到大排成一行,形成的序列称为 Farey 序列。 求出
所对应的 Farey 序列中第 小的数。
符合条件分数的个数。
sto smb orz
给出一个 smb 的亚线性做法。
有了上面的一些简单铺垫,我们不难想到在 S-B 树 上面进行二分。
那么对于一个最简分数
容易列出式子
对这个式子进行莫反,可以得到
我们发现前面是可以用杜教筛处理的(其实这道题中不用),而后面这一块是类欧几里得的板子,于是这个值我们是可以在
而我们如何去二分到这个值呢?
不难想到直接在 S-B 树 上面走,每次枚举走到左子树还是走到右子树,但是在
我们考虑把往一个方向走的连续段缩起来,也就是只枚举在哪些地方拐(eg:原先一直往左子树走,现在变成往右子树走),设
那么这样我们只会拐
总时间复杂度
ABC333G Nearest Fraction
给定一个小于
的正实数 和一个正整数 。 要求在满足 和 的前提下,找到使 最小的二元组 。 如果存在多个这样的二元组 ,输出 值最小的那个。 数据范围:
, 且最多包含 18 位有效数字。
首先
对于当前的区间
这样我们就可以不断减小这个区间,从而不断逼近
每一次搜索都用两个答案
用上一道题类似的倍增优化可以做到
双倍经验:P1298 最接近的分数
SP26073 DIVCNT1 - Counting Divisors
表示 的约数个数。 求
多测,
。
比较难的应用。
首先我们可以对式子进行转化
那么问题就转化成求
我们尝试把这个反比例函数画出图来,那么我们要求的其实就是下图中红色部分中的点数(from x义x):
不妨将其称作 R 区域。
我们希望用一些向量去不断拟合这个反比例函数,于是就想到了这个有理数系:S-B 树。
于是我们需要一个单调栈,里面维护的向量斜率单调递减,进行这样的操作:
- 每次从栈顶取出一个元素,不断加到当前点
上面,直到它差一步就走进 R 区域。
由于我们保证了斜率递减,且向量的斜率表示出来已经是最简分数,所以每一步都是对答案有贡献的,也就是图中棕色区域中的点(from x义x):
容易发现这样的点数一定不会多算,可以做到不重不漏。(在斜线下的点一定在反比例函数中,并且不会多)
如何计算点数?(我们假设每次上都要计算最下面的一行,从而不能计算上面的一行)
画出图来就是这个样子:
把它分成蓝色和橙色的两个部分,而橙色部分一定是上下对称的,所以假设当前向量为
- 不断弹出栈顶,使得攻栈顶刚好走不进 R 区域,称它为
。设最近一次弹的是 (有可能是上一步的栈顶向量)。
现在就是,加上
- 开始二分,用 S-B 树 得到中间向量
,我们称之为 。
-
如果
走一步不会进 R,那么 进栈并把 继续二分。 -
否则
- 如果
,那么继续二分肯定都走不出 R 了,直接停止二分,就是这个样子(from x义x):
- 如果
可以发现如果
- 否则 $l=mid$ 继续二分。
当
这样的时间复杂度是
注意到其他的一些东西:
-
最开始的点我们选择
,因为这样才能做到不重不漏(细节会少很多)。 -
初始栈中的向量是
和 。
这样我们就做完了。代码。
5.3 素数筛法
最重要的知识点,几乎所有题目都需要筛出
首先我们存在一个非常显然的做法——埃氏筛法。
埃氏筛用所有已经筛出的素数倍数标记合数:从
埃氏筛的精髓在于复杂度的证明:不超过
也就是
这说明埃氏筛的时间复杂度是
这里是来自 djq 的证明:
因为每个数只会被其素因子筛到,所以
根据
根据
两边同时取对数
因此
埃氏筛已经非常接近与线性了,但是它也还不是最优的,我们存在一个 线性筛素数 的做法。
它的思想和埃氏筛类似,埃氏筛的优秀之处在于仅用质数的倍数筛去合数,但合数会被多个质因子筛到。让每个合数仅被晒一次就能做到线性。
考虑用每一个合数 最小质因数 筛它:从
综上,有如下步骤:
-
从小到大遍历当前筛出的所有素数
,将 标记为合数。 -
若
,退出循环,因为 ,所以 的最小质因子为 不是 ,再筛就重复了。
时间复杂度线性,于是就可以通过 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 素性测试)
如何判断一个数是否是素数?
小素数的判定是容易的:直接试除法即可,时间复杂度
至于大素数判定……
费马曾提出一种方法:费马素性测试。
它基于费马小定理:设
为了测试
-
如果
不成立,那么 肯定不是素数。 -
如果
成立,那么 很大概率是素数,尝试的 越多, 是素数的概率就越大。
但是从第
对于某个
特别地,有一些合数,不管选什么
不过这种数很少,
由于上面 费马素性测试 的不完美,我们就引入了 Miller-Rabin 素性测试。
把费马素性测试改进一下,它是已知最快的随机素数测试算法。
原理也非常简单,就是费马素性测试排除 Carmicheael 数。
因为并且 Carmicheael 数在进行次数较多的二次探测定理逆否命题判定后,都会被判定出来。
如果
根据费马测试,如果
而由于
把
于是有:
所以计算出
容易发现,对于多个随机的基值
当
考虑做了
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)
判断完了素数,我们来尝试分解质因数。
众所周知,可以用试除法做到
所以我们考虑一种更高消的方法——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);
}
这个代码试图通过随即一些数并判断它们是否是
这个算法非常的劣质,但我们的 Pollard_rho 正是基于这个算法而来的,它的核心思想就是随机。
而我们知道 生日悖论:
如果现在有
而当
这个悖论告诉我们:组合随机采样比单个个体满足条件要高得多,这样可以提高正确率。
于是,Pollard 就提出了一个随机数的生成方法:
其中
这个函数的随机性非常好,但是进行了数次生成之后就会出现一个环,在坐标系中表示出来就变成了这个样子:
发现这个图像很像
那么考虑如何判环?
可以用 Floyd 的方法:
假设我们有
于是我们可以用这样随机出来的数相邻两个的差与
但是 gcd 的时间复杂度时
在计算时,我们将每次产生的
但是,如果某一时刻累积下来的样本的乘积为
每次计算的阈值可以倍增,并且加上一个上限,使用
至此,你就可以通过 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.任选两个不同的质数
2.计算
, 3.选取小于
,且与 互质的整数 4.计算整数
,使得 5.二元组
称为公钥,二元组 称为私钥。 当需要加密消息
时,(假设 是一个小于 的整数,因为任何格式的消息都可转为整数表示),使用公钥 ,按照 运算,可得到密文
对密文
解密时,用私钥 ,按照 运算,可得到原文
。算法正确性证明省略。 由于用公钥加密的密文仅能用对应的私钥解密,而不能用公钥解密,因此称为非对称加密算法。通常情况下,公钥由消息的接收方公开,而私钥由消息的接收方自己持有。这样任何发送消息的人都可以用公钥对消息加密,而只有消息的接收方自己能够解密消息。
现在,你的任务是寻找一种可行的方法来破解这种加密算法,即根据公钥破解出私钥,并据此解密密文。
。
题意写得非常长,但是并不难。
发现只要我们找到
P4714 「数学」约数个数和
给你一个正整数
,请计算 的 所有约数的 约数个数和。 答案可能很大,请输出对
取模的结果。
。
首先,对于
而当
这相当于一个狄利克雷卷积,而根据狄利克雷卷积卷积的性质,我们可以知道
于是现在问题就变成了如何求
发现这是简单的,等价于组合数学中在
根据对称公式,相当于
而
至于分解质因数,交给 Pollard_Rho 就可以了。代码。
6 离散对数问题
离散对数问题就是解决模
这样的
当
6.1 大步小步算法(BSGS)
大步小步算法全称 Baby Step Giant Step,简称 BSGS,适用于
整体思路用到了 根号平衡 的思想,设块长为
于是这个方程就可以被表示成
只要我们直接暴力枚举
根据欧拉定理(稍后会讲),有解就一定在
那么设块长为
于是你就可以通过 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;
}
容易发现你从小到大枚举时就可以得到最小的非负整数解,而关于
在这里还有一些板题:
-
P2485 [SDOI2011] 计算器 :综合一点的板题。代码。
P3306 [SDOI2013] 随机数生成器
最近小 W 准备读一本新书,这本书一共有
页,页码范围为 。 小 W 很忙,所以每天只能读一页书。为了使事情有趣一些,他打算使用 NOI2012 上学习的线性同余法生成一个序列,来决定每天具体读哪一页。
我们用
来表示通过这种方法生成出来的第 个数,也即小 W 第 天会读哪一页。这个方法需要设置 个参数 ,满足 ,且 都是整数。按照下面的公式生成出来一系列的整数:
其中
但是这种方法可能导致某两天读的页码一样。
小 W 要读这本书的第
页,所以他想知道最早在哪一天能读到第 页,或者指出他永远不会读到第 页。
是质数
考虑先全部左移一维,那么得到的数其实就是
于是我们就可以将
这就可以用 BSGS 直接完成啦——注意特判
总时间复杂度
P4884 多少个 1?
给定整数
和质数 ,求最小的正整数 ,使得 ( 个 ) 。 说人话:就是
。
, ,保证 是质数。
a 如果我们直接把
我们考虑将这个数表示成一个数,也就是它乘
那么此时这个方程就变成了
于是这个东西就可以直接用 BSGS 完成了。
注意可能在乘的过程中爆 long long。代码。
CF1106F Lunar New Year and a Recursive Sequence
有一串
个数的数列,给你 。当 时: 已知
,问 可能是多少。
, 。
会用到原根相关知识,若不会原根建议先看后面原根部分。
有关于
而这种需要让我们想到了 原根,众所周知,
把指数提下来,同时模数变成了
那么只要我们能求得
发现这个东西刚好是矩阵乘法的形式,也就是我们可以构造
从而得到
于是当我们知道
而由于前面都是
其中
由于
现在问题就转化成如何求出
于是这道题就做完了,实现时注意时模
6.2 扩展大步小步算法(exBSGS)
接下来我们来讨论更为普遍的问题
从已知到未知: 由于
我们运用同余方程的运算法则,我们把两边同时除以
由于
然而这时的
而注意到每除依次,
容易发现这样的时间复杂度是
这样就可以通过 P4195 【模板】扩展 BSGS/exBSGS 和 SP3105 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】快乐肥宅
给定
组 ,求关于 的方程组 (定义
)在 范围内的最小整数解,或判断在这个范围内无解。
。
难绷。
容易发现,我们的解题过程分成两部分:求解每一个方程的解 和 求线性同余方程组的解。
首先考虑第一部分,如何求解每一个方程的解呢?
根据 exBSGS 的求解过程可以知道,解的情况一定分成了三种:无解、唯一解和无穷解。
画出图来,解一定是一个
其中
而进入环上就变成了无穷解,而这个解是形如线性同余方程;
反之,没有出现过的就是无解。
尾巴的长度是可以直接求
反之对于只有一个解的情况,我们直接记它为答案,最后特判一下就可以了。
接下来考虑第二部分,求
由于不保证模数互质,所以这个需要用 exCRT 解决,而
考虑 exCRT 的过程,中间每一次我们都是 ans+=x*M
,那么只要这次的
所以这个时候我们只需要判断当前的解对于后面的方程是否都成立即可。
这样就做完了,时间复杂度为
本文作者:H_W_Y
本文链接:https://www.cnblogs.com/H-W-Y/p/18203757/NumberTheory2
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步