数论学习笔记
wangyichen_0711 · · 算法·理论
updata on 2026/8/26 : 修改格式问题
下文内容是在大纲中 NOI 难度(可能包含)以下的数论算法的内容、证明、应用及例题。
这是我第四遍听数论了,再不会本人真该退役了同志们。
感谢 @Mingoal 老师的讲解,文中的大部分内容来自老师的课件,还有机房大佬 @Natural_Selection 对我没听懂的裴蜀定理的讲解证明,文章在这,我的文章也有引用这里的部分内容,一定要先把它看完。
扩展欧几里得 (exgcd)
内容
通过上面的文章,我们了解到了只要满足裴蜀定理的方程,就必定有解,但我们更希望能求出这样的一组特解到底是什么,于是人们在欧几里得算法的基础上推导出了扩展欧几里得(exgcd)。
:::warning[什么是欧几里得算法?] 欧几里得算法,又称辗转相除法,通过递归的方式求解两个数的最大公约数,即:
当
代码如下实现:
int gcd(int a,int b){b==0?a:return gcd(b,a%b);}
:::
- 边界:
b=0 时\gcd(a,0)=a ,取(x=1,y=0) ,即a\times 1+0\times 0=a 成立。 - 递归假设:已求出
bx'+(a \bmod b)y'=\gcd(b,a\bmod b) 的解为:
- 带入
a \bmod b =a-\lfloor \frac{a}{b}\rfloor b ,展开并进行整理:
原方程
- 对照原方程,此时方程的解又一次转化为了:
递归层数为
这里非常抽象难懂,可以结合后文的例题代码理解。
应用
- 应用 1:解线性不定方程
无整数解 等价于
- 应用 2:解同余方程
exgcd 求出特解
最小正整数解为
这就是后文 逆元 求解的同余方程形式。
可能你会怀疑如果
- 应用 3:求乘法逆元
- 与加减乘相容:例如
8\bmod 7 *3 =1*3=3 且8*3\bmod7=24\bmod 7=3 。
最好应养成如下三个习惯(用代码表示):
1. 加减乘三种运算时取模应为:
- 加法:
(a+b)%m=(a%m+b%m)%m - 减法:
(a-b)%m=(a%m-b%m)%m - 乘法:
(a*b)%m=((a%m)*(b%m))%m
- 将结果归一到
[0,m-1]
- C++ 对
%对负数返回负余数,需调整return (x%m+m)%m;
- 乘法先取模防溢出
- 连乘前先对每个因子取模,中间结果也每步取模:
res=res*a%m; - 当
m 很大时,可以使用类似高精度乘法的龟速乘或者_int128(后文讲解 P3868 需要使用,但貌似同学说__int128是有误的?)
内容 of 逆元
像上文所说,除法并不适配取模运算,但我们有时也需要使用它,于是我们会寻找一个在取模运算下仍能正常使用的乘法。
于是我们有了一下定义:
若整数
其作用即为在模意义下进行除法,把除法变为乘法:
性质 of 逆元
1. 存在条件:
代码实现 of 逆元
- 线性递推(批量)
推导:令 inv[i]=(p-p/i)*inv[p%i];,由规定不难发现
//今夜は月が綺麗ですね
#include <bits/stdc++.h>
#define LL long long //[(p-p/i)可能炸int]
using namespace std;
LL inv[3001000];
signed main(){
//[p必须为质数且n<p,否则p%i>=i导致失败!!]
int n;LL p; cin>>n>>p;
inv[1]=1;
for(int i=2;i<=n;i++)inv[i]=(p-p/i)*inv[p%i]%p;
for(int i=1;i<=n;i++)cout<<inv[i]<<" ";
return 0-0;
}
- 扩展欧几里得(单点)
要点:不难发现求逆元等价于
//今夜は月が綺麗ですね
#include <bits/stdc++.h>
using namespace std;
LL exgcd(LL a,LL b,LL &x,LL &y){
if(b==0){x=1,y=0;return a;}
LL g=exgcd(b,a%b,y,x);
y-=a/b*x;
return g;
}
LL inv(LL a,LL m){
LL x,y;
exgcd(a,m,x,y);
return (x%m+m)%m;
}
- 费马小定理(
m 为质数)
//今夜は月が綺麗ですね
#include <bits/stdc++.h>
#define LL long long
using namespace std;
LL ksm(LL a,LL b,LL p){
LL ans=1;
while(b){
if(b&1)ans=ans*a%p;
a=a*a%p,b>>=1;
}
return ans;
}
LL inv(LL a,LL p){
return ksm(a,p-2,p);//不明白这里的后文会讲解
}
费马小定理
内容
或者是一般形式
对任意整数
这个定理被称为费马小定理。
:::info[证明(选读)]
- 考虑集合
S=\set{a,2a,3a,...,(p-1)a} :若ia \equiv ja (\bmod p) \set{0<i<j<p-1} ,则p \mid (j-i)a ,再由p 不整除a 且p 为质数,发现与0<j-i<p 矛盾。 - 故
S 中元素模p 两两不同余且均不为 0,即恰好是\set{1,2,3,...,p-1} 的一个序列。 -
两边取乘积: a^{p-1}(p-1)!\equiv (p-1)! (\bmod p) 。因为\gcd((p-1)!,p)=1 ,两边约去(p-1)! ,即得a^{p-1}\equiv 1 (\bmod p) 。
推论及应用
- 推论 1:模质数求逆元
由
- 推论 2:指数降幂
- 令
res 初始为n , 从2 试除到\sqrt{n} ;找到每一个质因子p :res=res/p*(p-1) ,并除尽p 。 - 试除结束后若剩余
x>1 则说明剩下的这个x 是大于\sqrt n 的质因子,res=res/x*(x-1) 。
时间复杂度
- 批量求欧拉函数
应用线性筛的思想,不难发现,对于某个数
我们从小到大枚举数字并分类讨论,如果是质数由性质可知答案,否则暴力向上翻倍枚举,直到找到翻倍的数
//今夜は月が綺麗ですね
#include <bits/stdc++.h>
using namespace std;
int pri[1001000],cnt,phi[1001000];
bool notpri[1001000];
int eulerSieve(int n){
phi[1]=1;
for(int i=2;i<=n;i++){
if(!notpri[i])pri[++cnt]=i,phi[i]=i-1;
for(int j=1;j<=cnt&&i*pri[j]<=n;j++){
int p=pri[j],ip=i*p;
notpri[ip]=1;
if(i%p==0){phi[ip]=phi[i]*p;break;}
phi[ip]=phi[i]*(p-1);
}
}
}
内容 of 欧拉定理
这个公式名为欧拉定理。
可以理解成素数个数在一定数字内不断循环。
当
:::info[证明(选读)] 使用了与费马小定理相似的证明套路。
- 设
1\leq b_1<b_2<...<b_{\varphi(m)}\leq m 是1...m 中所有与m 互质的数(共有\varphi(m) 个) - 对每个
i 考察ab_i :由\gcd(a,m)=1 且\gcd(b_i,m)=1 可知\gcd (ab_i,m)=1 ;且ab_i 模m 两两不同 [若ab_i=ab_j \equiv s (\bmod m) ] ,则m \mid a(b_i-b_j) ,由\gcd(a,m)=1 得m \mid (b_i-b_j) ,矛盾。 -
故 \set{ab_1,ab_2,...,ab_{\varphi(m)}} 模m 后认为\set{b_1,b_2,...,b_{\varphi(m)}} 的序列。取乘积a^{\varphi(m)}\set{b_1,b_2,...,b_{\varphi(m)}}\equiv \set{b_1,b_2,...,b_{\varphi(m)}} (\bmod m) ;因为\Pi b_i 与m 互质可约去,得a^{\varphi(m)}\equiv 1 (\bmod m)
内容 of 扩展欧拉定理
这个在欧拉定理公式基础上变形得来的公式名为扩展欧拉定理。
其用途在读不完的超大整数上。
-
- 边读边做两件事:与
\varphi(m) 比较大小,判断是否b\geq \varphi(m) ;对\varphi(m) 取摸得到b' 。 - 若
b\geq \varphi(m) :ans=a^{b'+\varphi(m)} ,否则ans = a^b ,用快速幂实现。
例题 of 扩展欧拉定理
- P5091 【模板】扩展欧拉定理模版题,代入定理即可。
//今夜は月が綺麗ですね
#include <bits/stdc++.h>
#define LL long long
using namespace std;
LL a,m,phi=1,now;
bool flag=0;
LL ksm(LL x,LL y){
LL ans=1;
for(;y;y>>=1,x=x*x%m){if(y&1)ans=ans*x%m;}
return ans;
}
signed main(){
cin>>a>>m;
a%=m;LL n=m;
for(LL i=2;i*i<=n;i++){
if(n%i)continue;
phi*=i-1;n/=i;
while(n%i==0)phi*=i,n/=i;
}
if(n>1)phi*=n-1;
char ch;
while((ch=getchar())<'0'||ch>'9');
while(now=now*10ll+(ch-'0'),(ch=getchar())>='0'&&ch<='9')if(now>=phi)flag=1,now%=phi;
if(now>=phi)flag=1,now%=phi;
if(flag)now+=phi;
cout<<ksm(a,now);
return 0-0;
}
- P3934 [Ynoi Easy Round 2016] 炸脖龙 I
不难发现,如果一个个去乘一定会炸时间,考虑使用扩展欧拉定理进行降幂。
每次递归的处理靠左一点的幂次,直到相同。
对于求和,我们可以考虑使用树状数组维护。
//今夜は月が綺麗ですね
#include <bits/stdc++.h>
#define LL long long
using namespace std;
LL phi[20000005],prime[20000005],cnt,f[500005],a[500005],n,m;
bool isnotpri[20000005];
LL lowbit(LL x){return x&-x;}
void upd(LL x,LL k){while(x<=n){f[x]+=k;x+=lowbit(x);}}
LL query(LL x){LL sum=0;while(x>=1){sum+=f[x];x-=lowbit(x);}return sum;}
LL ksm(LL a,LL b,LL p){
if (p==1)return 0;
if (a==0||a==1)return a;
LL ans=1;bool flag=0;
if(a>=p){flag=1;a%=p;}
if(b>=32)flag=1;
else{LL tmp=1;for(int i=0;i<b;i++){tmp*=a;if(tmp>=p){flag=1;break;}}}
if(b>=phi[p])flag=1,b=b%phi[p]+phi[p];
while(b){
if(b&1)ans=ans*a%p;
a=a*a%p,b>>=1;
}
if(flag)ans+=p;
return ans;
}
LL solve(LL l,LL r,LL p){
LL tmp=query(l);
if(p==1) return 1;//重要特判!!
if(l==r) return tmp>=p?tmp%p+p:tmp;
return ksm(tmp,solve(l+1,r,phi[p]),p);
}
signed main(){
cin>>n>>m;
for(LL i=1;i<=n;i++){cin>>a[i];upd(i,a[i]);upd(i+1,-a[i]);}
isnotpri[1]=1;phi[1]=1;
for(LL i=2;i<=20000000;i++){
if(!isnotpri[i]){prime[++cnt]=i;phi[i]=i-1;}
for(LL j=1;j<=cnt;j++){
if(i*prime[j]>2e7)break;
isnotpri[i*prime[j]]=1;
if(i%prime[j]==0){phi[i*prime[j]]=phi[i]*prime[j];break;}
else phi[i*prime[j]]=phi[i]*(prime[j]-1);
}
}
for(LL i=1;i<=m;i++){
LL opt;cin>>opt;
LL l,r,x;cin>>l>>r>>x;
if(opt==1)upd(l,x),upd(r+1,-x);
else cout<<solve(l,r,x)%x<<endl;
}
return 0;
}
- P2158 [SDOI2008] 仪仗队
不难发现,只要任意一个点
对于每个非终点的横坐标
//今夜は月が綺麗ですね
#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int N=40010;
bool isnpri[N];
int cnt,pri[N],phi[N];
signed main(){
int n;
cin>>n;
for(int i=1;i<=n;i++)phi[i]=i;
for(int i=2;i<=n;i++)
if(phi[i]==i)
for(int j=1;j*i<=n;j++)
phi[i*j]=phi[i*j]/i*(i-1);
LL ans=0;
for(int i=1;i<n;i++)ans+=phi[i];
cout<<(n==1?0:ans*2+1);
}
数论分块
内容
对于固定的
因此当我们求
:::info[为什么只有
- 当
i \leq \sqrt{n} 时,i 本身有且仅有\leq \sqrt{n} 个,因此\lfloor\frac{n}{i}\rfloor 当然最多只有\sqrt n 个不同取值。 - 当
i > \sqrt{n} 时,\lfloor\frac{n}{i}\rfloor \leq \frac{n}{i}<\sqrt{n} ,取值落在[0,\sqrt{n}] 的范围内,也至多只有\sqrt n 个不同取值。 -
合起来,不同取值的个数一定 \leq 2\sqrt{n} 。
我们还可以的得到用来计算右端点的公式(取值相同的一段区间)。
若左端点为
代码实现
LL block(LL n){
LL ans=0;
for(LL l=1,r;l<=n;l=r+1){
LL t=n/l;
r=n/t;//右端点
ans+=(t-l+1)*t;//块内贡献
}
}
当和式中同时出现
取模拆除法:
例题
- P2261 [CQOI2007] 余数求和
注意到,原式进行如下变化:
由上文定义可知后面一项中
对于每一个块
最终时间复杂度
//今夜は月が綺麗ですね
#include <bits/stdc++.h>
#define LL long long
using namespace std;
LL n,k,ans;
signed main(){
cin>>n>>k;
for(LL l=1,r,t;l<=n;l=r+1){
t=k/l;
r=t?min(k/t,n):n;
ans-=t*(r-l+1)*(l+r)/2;
}
cout<<ans+n*k;
return 0;
}
离散对数 (BSGS 大步小步算法)
内容
为了求解形如
令
- 不断枚举
j=0,1,...,m-1 ,把ba^j\bmod p 存入哈希表,值为j (重复则保留j 最大的)。 (Baby step) - 枚举
i=1,2,...,m ,查询a^{im} 是否在哈希表中;命中j 则解为x=im-j ,i 从小到大第一个即为最小解。 (Giant step) - 空间时间复杂度
O(\sqrt p) ,i 从1 到m 覆盖了x 的整个取值范围[0,p) 。
于是,这种算法便被称为大步小步算法(BSGS)。
可以把第 1 步具象成从终点往中间连边,第 2 步具象成从起点往中间找边,一种非常经典的 折半搜索(Meet in Middle) 思想。
例题
- P3846 【模板】BSGS / [TJOI2007] 可爱的质数
纯板子,不作题目内容讲解。
//今夜は月が綺麗ですね
#include <bits/stdc++.h>
#define LL long long
using namespace std;
LL BSGS(LL a,LL b,LL p){
a%=p,b%=p;
if(b==1)return 0;
LL m=ceil(sqrt((double)p));
map<LL,LL>mp;//[unordered_map会更快]
LL cur=1,step=cur;//cur代表Baby Step,重复保留大j (步骤1)
//giant从i查a^(im)(步骤2)
for(LL j=0;j<m;j++)mp[cur*b%p]=j,cur=cur*a%p;
for(LL i=1;i<=m;i++,cur=cur*step%p)
if(mp.count(cur))return i*m-mp[cur];
return -1;
}
signed main(){
LL a,b,p;
cin>>p>>a>>b;
LL ans=BSGS(a,b,p);
if(ans==-1)cout<<"no solution";
else cout<<ans;
return 0-0;
}
(扩展) 中国剩余定理 (CRT / exCRT)
内容 of CRT
当我们遇到了同余方程组应如何求解呢?我们进行如下构造:
- 现在有同余方程组
x \equiv b_i (\bmod a_i),i=1...n ,保证a_i 两两互质。 - 令
M=a_1\times a_2 \times ...\times a_n ,即所有模数之积。 - 对每个
M_i=\frac{M}{a_i} ,表示去掉了第i 个模数,求M_i 模a_i 的逆元t_i (M_it_i\equiv 1\bmod a_i ,exgcd)。 - 最后构造答案:
这样的构造方式被称为中国剩余定理(CRT)。
性质 of CRT
:::info[为什么
| 所以 |
|---|
:::info[为什么解在模