数论初步
参考资料#
《算法竞赛进阶指南》
基本数论概念#
整除、因数、倍数什么的就不用说了吧。
数论函数#
指定义域为正整数的函数。下面讨论的『函数』如非特殊说明,都指数论函数。
积性函数#
定义#
若有 ,则称 为积性函数。
若有 ,则称 为完全积性函数。
性质#
若 都是积性函数,则以下函数也是积性函数:
前三个证明显然,第四个其实是这两个函数的狄利克雷卷积,后面会讲。
欧几里得算法#
内容及其证明#
求 gcd 的常用方法。其内容为:
证明如下:
当 时,定理显然成立。
当 时,设 ,其中 ,即 。因为 都有 ,所以 即 。于是我们得到结论:。因此, 与 的公约数集相对于 与 的公约数集是没有改变的,那么两个集合中最大的数自然不变。
证毕。
算法应用及其时间复杂度证明#
开始前,先来证明一个结论:当 时,。
注:证明过程中的 为任意不小于 1 的正实数。
(一个大于等于 1 的数的一半一定小于其整数部分)
根据欧几里得算法,代码很容易得出:
int gcd(int a,int b){
return b?gcd(b,a%b):a;
}
其时间复杂度为 。证明如下:
在求 时,会出现下面两种情况:
- ,此时会由
gcd(a,b)
递归到gcd(b,a)
。- ,此时会由
gcd(a,b)
递归到gcd(b,a%b)
。这样就使得其中一个数至少折半。因此这一情况最多发生 次。由于前者发生后一定会发生后者,所以前者的发生次数一定不多于后者,所以总的时间复杂度仍为 。
欧拉函数#
内容#
欧拉函数为 ,表示小于等于 的与 互质的数的个数。即:
性质#
性质一#
若在算数基本定理中,,则:
证明:容斥原理。设每个 的质因子 在 中的倍数集合为 。则有:
又因为我们知道 ,于是就可以用容斥的公式:
我们还知道,。于是:
性质二#
欧拉函数是积性函数。
证明:(其中 ,在唯一分解定理中,,)。
https://www.zhihu.com/question/410737433
性质三#
证明(运用了莫比乌斯反演,可以先把整篇文章看完再回过头来看):
upd: 我太 naive 了。现在告诉大家一种更简单的证明方法。
这个式子其实在算不超过 的与 互质的数的和。我们知道 ,所以对于每个 ,都有 ,它们的和等于 ,所以事实上就是 个 相加。
埃氏筛与欧拉筛#
埃氏筛#
这种筛法虽然时间复杂度逊于欧拉筛,但是对于题目的一些特殊要求能够更好地应付。例如 筛一段区间内的质数 就只能用埃氏筛。
埃氏筛的代码如下:
for(ll i=2;i<=N;i++){
if(!vis[i]){
p.push_back(i);
if(i*i<=N){
for(ll j=i*i;j<=N;j+=i)vis[j]=1;
}
}
}
可以看出,埃氏筛是对于每个质数,把能整除它的所有和数全部标记一遍。但是一个小于 的合数必定有比 更小的质因子,所以它肯定已经被筛过,没有必要再筛。
如果想要筛一段区间 内的质数,只需要用 内的质数去筛就可以了。
设需要筛出的质数范围为 ,则其时间复杂度为 ,证明需要用微积分,然而我不会,会微积分的可以取 OI Wiki 看证明。 现在会了
欧拉筛#
现在我们想筛出 到 的所有质数。
欧拉筛的核心思想是从大到小累计质数,以达到每个合数只筛一次的目的。算法用一个数组来记录每个数的最小质因子。设这个数组为 ,我们可以用以下方式维护它:
- 从小到大用 把 到 的每个数扫一遍。
- 如果 没有被修改过,说明 没有被筛,所以 是质数,其最小质因子为其本身,即执行 。
- 无论 是不是质数,都需要在 的基础上累计质因子。即扫描不大于 的每个质数 ,。
注意第三点,因为质因子是从大到小累积的,所以 一定是 的最小质因子。
该算法从 到 的每个数都只会被遍历一次,筛一次,所以时间复杂度为 。
P3383 线性筛质数模板的核心代码:
void init(int N){
for(int i=2;i<=N;i++){
if(!v[i]){v[i]=i;p.push_back(i);}
for(int pr:p){
if(pr>v[i]||pr>N/i)break;
v[pr*i]=pr;
}
}
}
线性筛欧拉函数#
线性筛过程中,假设我们正在用一个数 和一个质数 筛 ,且已知 ,需要求出 。设在唯一分解定理中,
- 当 时,
- 当 时,根据积性函数的定义,
然后就能求出 中的每一个整数的欧拉函数值了。只需在上面的程序中稍加修改:
inline void init(int n){
for(int i=2;i<=n;i++){
if(!v[i]){v[i]=i;phi[i]=i-1;p.push_back(i);}
for(int pr:p){
if(pr>v[i]||pr>n/i)break;
v[pr*i]=pr;
phi[pr*i]=phi[i]*(pr==v[i]?pr:phi[pr]);
}
}
}
线性筛任意积性函数#
已知 是一个积性函数。设在唯一分解定理中,。则有:
因此,只要在欧拉筛时,统计每个数最小质因数 及其次数 ,就可以递推计算积性函数:
以后,在讨论积性函数时,只讨论其在 的取值然后相乘的方法,是比较常见的。
例题#
P5481 [BJOI2015] 糖果#
你可能会问这不是一道组合计数问题吗,为啥放到数论这里。
因为这道题的难点不在于计数,而在于任意模数。
计数式子插板法随便搞搞就得出来了,是
所以任意模数怎么办呢?如果直接 exgcd 算很可能出现逆元不存在的情况,而且还不一定是模数的倍数,所以答案不一定是 。
然后我就忍不住看了题解,发现只要把 分解质因数,然后把上面的式子除掉这些质因数就行了。
这里有一个分解阶乘质因数的技巧:枚举所有小于等于 的质数 ,然后计算 在 中出现的次数。这个次数就等于能整除 的数的数量加上能整除 的数的数量加上能整除 的数的数量,等等。这样一来,如果一个数含有 个 ,其贡献就为 ,所以这个算法是正确的。故 在 中出现的的次数为:
代码如下:
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=1e5+5;
int pr[N],c[N],d[N],tot;int P,v[N];
inline void init(int m){
const int n=N-5;
for(int i=2;i<=n;i++){
if(!v[i])pr[++tot]=v[i]=i;
for(int j=1;j<=tot;j++){
if(pr[j]>v[i]||pr[j]>n/i)break;
v[pr[j]*i]=pr[j];
}
}
for(int i=1;i<=tot;i++){
if(m<i)break;
ll x=pr[i];
while(m>=x){
c[i]+=m/x;
x*=pr[i];
}
}
}
inline ll dow(ll a,int b){
ll ret=1;
while(b--){
ret=(ret*a)%P;
a--;
}
return ret;
}
int n,m,K;
int main(){
scanf("%d%d%d%d",&n,&m,&K,&P);
init(m);ll ans=1;
for(int i=K;i<K+m;i++)d[i-K]=i;
for(int i=1,p;i<=tot;i++){
p=pr[i];if(!c[i])break;
for(int j=(K%p?(K/p+1)*p:K);j<K+m;j+=p){
while(c[i]&&d[j-K]%p==0)d[j-K]/=p,c[i]--;
}
}
for(int i=K;i<K+m;i++)ans=(ans*d[i-K])%P;
printf("%lld\n",dow(ans,n));
return 0;
}
P2158 仪仗队#
一个人 能被看到,当且仅当 或 (这里下标从 0 开始)。然后因为正方形是沿对称轴对称的,所以我们可以先算出答案的不严格一半,即 的情况。
于是,问题转化为:求 。
线性筛一下就好了,答案为 ,其中 是减去 的情况(因为这个点在对称轴上) 是加上 的情况。
细节:注意 ,需要提前赋值。注意输入要减一,而且要特判输入等于一的情况。
该算法的时间复杂度为 。
我的代码:
#include<cstdio>
#include<vector>
using namespace std;
const int N=4e4+5;
int v[N],phi[N];
long long ans;
vector<int>p;
inline void init(int n){
ans=phi[1]=1;
for(int i=2;i<=n;i++){
if(!v[i]){v[i]=i;phi[i]=i-1;p.push_back(i);}
for(int pr:p){
if(pr>v[i]||pr>n/i)break;
v[pr*i]=pr;
phi[pr*i]=phi[i]*(pr==v[i]?pr:phi[pr]);
}
ans+=phi[i];
}
}
int n;
int main(){
scanf("%d",&n);
init(n-1);
printf("%lld",(n-1)?((ans-1)<<1)+3:0ll);
return 0;
}
同余#
若 ,则称 。
同余类和剩余系#
定义#
对于 ,将集合 称为模 的一个同余类,简记为 。
个同余类构成 的完全剩余系(即 )。
到 中与 互质的数所代表的剩余系有 个,它们构成 的简化剩余系。
性质#
简化剩余系关于模 乘法封闭。
证明#
设 是 简化剩余系中的两个数,。
则 ,所以
根据定义,,所以 。
根据欧几里得算法公式,,则简化剩余系关于模 乘法封闭。
证毕。
欧拉定理和费马小定理#
内容#
欧拉定理:若正整数 互质,则 。
费马小定理:若 是质数,则对于任意整数 ,有 。
证明#
设 的简化剩余系为 。根据定义,。
即
, 都在 的简化剩余系中。
设 和 是相同的同余类,则 ,即
即
于是我们得到一个成立的命题:『如果 和 是相同的同余类,那么』。因此它的逆否命题『如果 ,那么 和 不是相同的同余类』也成立。
于是:
至于费马小定理,其实就是欧拉定理的一个特例(除了 的情况,不过这种情况下费马小定理显然成立)。
欧拉定理的推论#
内容#
证明#
扩展欧拉定理#
例题#
P5091 【模板】扩展欧拉定理#
套公式即可。代码是很久以前写的,所以码风可能和现在不一样。
#include<cctype>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
inline ll read(){
char c=getchar();ll x=0;bool f=0;
for(;!isdigit(c);c=getchar())f^=!(c^45);
for(;isdigit(c);c=getchar())x=(x<<1)+(x<<3)+(c^48);
return f?-x:x;
}
inline ll getB(ll mod){
char c=getchar();ll b=0;bool f=0;
for(;!isdigit(c);c=getchar());
for(;isdigit(c);c=getchar()){
b=(b<<1)+(b<<3)+(c^48);
if(b>mod)b%=mod,f=1;
}
return f?b+mod:b;
}
inline ll phi(ll x){
ll res=x;
for(register ll i=2;i*i<=x;i++){
if(x%i==0){
res=res*(i-1)/i;
while(x%i==0)x/=i;
}
}
if(x>1)res=res*(x-1)/x;
return res;
}
inline ll quickPower(ll a,ll b,ll n){
ll res=1;
while(b){
if(b&1){
res=(res*a)%n;
}
a=(a*a)%n;
b>>=1;
}
return res;
}
ll a,m,phim,b;
int main(){
a=read();m=read();
phim=phi(m);
b=getB(phim);
printf("%lld",quickPower(a,b,m));
return 0;
}
数论分块#
过程#
对于形如 的式子,由于 的取值至多只有 种,因此可以把和式分块计算,而 使用前缀和处理。
结论:值 所在块的右端点为
我们也可以类似地搞一个二维(或多维)数论分块,用于求解类似 的式子。这时我们应该取右端点为所有维度右端点的最小值。例如二维数论分块应为 r=min(n/(n/i),m/(m/i))
。
显然,二维数论分块的时间复杂度仍然是 的(假设 和 同级)。
证明#
令 。
例题#
UVA11526#
题意
求 。 组数据。
分析
模板题。按照上面的流程即可。注意边界情况和运算顺序。
时间复杂度为 。
代码
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
ll H(ll n){
ll ret=0;
for(ll l=1,r=0;l<=n;l=r+1){
r=n/(n/l);ret+=(r-l+1)*(n/l);
}
return ret;
}
int main(){
ll T,n;
scanf("%lld",&T);
while(T--){
scanf("%lld",&n);
printf("%lld\n",H(n));
}
return 0;
}
狄利克雷卷积#
从现在开始,数论函数一律使用粗黑体(如 )或希腊字母(如 )。
定义两个数论函数的加法为 ,点乘为 。下面将省略点乘符号。
当 是完全积性函数时,。
定义#
运算
为狄利克雷卷积。也可以写成:
性质#
有单位元 ,即 。
当 都是积性函数时, 也是积性函数。
一个积性函数的逆也是积性函数。
对于任意 的函数 ,存在逆元 使得 。
若 是 的逆元,只需要满足以下式子即可:
证明#
挑一些不那么明显的。
结合律:
这就与三个函数的顺序无关了。
求 的逆元 :
其他先鸽了。
一些常见的数论函数及其性质#
定义#
定义 为 的逆元。即 。
性质#
定义中提到的所有函数均为积性函数。其中前两个是完全积性函数。
其实只要记住前三个,后面的都可以自己推出来。
这个式子自己代入定义就很容易得到了。
证明#
前两个都很显然。这里证明第三个。设 为任意质数。
由于 是积性函数,所以有:
倒数第二个可以用前面提到的欧拉函数的性质证明。以后做题推式子要注意不要忘记前面提到的公式。
最后一个可以通过分类讨论质数在 中的分布情况证明。
莫比乌斯反演#
定理#
若 ,则有 。
当然,一般的题目肯定不会直接写出卷积的形式。
所以我们这么写:
魔力筛#
虽然在网上没看见过这种叫法,倒是搜到了魔力筛让天价药物降价,但机房都这么叫,我也暂且这么叫吧。
这是一个可以在 的时间复杂度求出 任意数论函数 与 的狄利克雷卷积的算法。当然,必须保证这个数论函数能被提前计算出来。
设 ,其中 只含前 种质因子。则有:
需要滚动数组优化。
虽然可以被解释为高维差分,但我不会。
其实这个 DP 方程也比较好理解。第一种情况显然正确。我们来看看第二种:
其中 只包含前 种质因子, 只包含前 种质因子。我们知道,每多一个质因子, 的取值就会乘以 ,因此上式的意义是:强制不选 ,然后乘以 累加在答案上。如果含有平方因子,则值为 ,不影响答案。
复杂度和埃氏筛一样。
参考代码:
inline void calc(int n){
for(int i=1;i<=n;i++)g[i]=f[i];
for(int p:pr){
for(int i=n/p;i>=1;i--){
g[i*p]=g[i*p]-g[i];
}
}
}
应用#
练习题#
题目描述
给定 ,求
题目分析
我们知道 。
有一个常见的套路:若要求 ,可以先找到一个数论函数 ,这样子就有:
于是解决这道题就变得很简单了。我们知道 。所以好像并不需要莫比乌斯反演:
然后就可以用二维数论分块搞了。不过现在应该不会出这么套路的题了吧。
[国家集训队]Crash的数字表格 / JZPTAB#
题目描述
给定 ,求
对 取模的结果。
数据范围:。
题目分析
令 ,则 。
显然是积性函数,并且在质数幂处很容易求。因此我们可以线性计算之。
时间复杂度为 。数论分块好像没有必要,如果不用数论分块甚至可能会快一点
代码
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=1e7+5,P=20101009;
int v[N],t[N];
vector<int>pr;
ll inv[N],g[N],s[N];
inline void init(int n){
inv[1]=1;g[1]=1;s[1]=1;
for(int i=2;i<=n;i++){
inv[i]=(P-P/i)*inv[P%i]%P;
if(!v[i]){
g[i]=(inv[i]+P-1)%P;
pr.push_back(t[i]=v[i]=i);
}else{
if(i==t[i])g[i]=(inv[i]-inv[i/v[i]]+P)%P;
else g[i]=g[t[i]]*g[i/t[i]]%P;
}
for(int p:pr){
if(p>v[i]||p>n/i)break;
v[i*p]=p;t[i*p]=(v[i]==p?t[i]*p:p);
}
s[i]=(s[i-1]+g[i]*i%P*i%P)%P;
}
}
inline ll calc(int n,int m){
ll ret=0;
for(int l=1,r;l<=n;l=r+1){
r=min({n,n/(n/l),m/(m/l)});
ret=(ret+(s[r]-s[l-1]+P)%P*(n/l)%P*(n/l+1)%P*(m/l)%P*(m/l+1)%P)%P;
}
return ret*inv[4]%P;
}
int main(){
int n,m;scanf("%d%d",&n,&m);
if(n>m)swap(n,m);init(n);
printf("%lld\n",calc(n,m));
return 0;
}
[SDOI2017]数字表格#
题目描述
给定 ,求
对 取模的结果。其中 表示斐波那契数列的第 项。
有 组数据。数据范围:,。
题目分析
我们定义 ,。则:
这个式子可以外层数论分块,内层暴力枚举计算。
单次询问的时间复杂度为 。注意对 的处理。
另一种方法是使用类似魔力筛的方法计算内层,从而使预处理的时间复杂度降到 ,请自行参阅题解。
代码
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=1e6+5,P=1e9+7;
inline ll _pow(ll a,ll b){
ll ret=1;a%=P;
while(b){if(b&1)ret=(ret*a)%P;a=a*a%P;b>>=1;}
return ret;
}
int mu[N],v[N];
ll F[N],G[N];
vector<int>pr;
inline void init(){
mu[1]=1;F[0]=0;F[1]=1;G[0]=G[1]=1;
const int n=N-5;
for(int i=2;i<=n;i++){
G[i]=1;F[i]=(F[i-1]+F[i-2])%P;
if(!v[i])pr.push_back(v[i]=i),mu[i]=-1;
for(int p:pr){
if(p>v[i]||p>n/i)break;
v[i*p]=p;
if(v[i]==p)mu[i*p]=0;
else mu[i*p]=mu[i]*mu[p];
}
}
ll mF;
for(int i=2;i<=n;i++){
mF=_pow(F[i],P-2);
for(int j=i;j<=n;j+=i){
if(mu[j/i]==-1)G[j]=G[j]*mF%P;
else if(mu[j/i]==1)G[j]=G[j]*F[i]%P;
}
}
for(int i=2;i<=n;i++){
G[i]=G[i-1]*G[i]%P;
}
}
int main(){
int T,n,m;scanf("%d",&T);
ll ans;init();
while(T--){
scanf("%d%d",&n,&m);
if(n>m)swap(n,m);
ans=1;
for(int l=1,r;l<=n;l=r+1){
r=min(n/(n/l),m/(m/l));
ans=ans*_pow(G[r]*_pow(G[l-1],P-2)%P,(ll)(n/l)*(m/l))%P;
}
printf("%lld\n",ans);
}
return 0;
}
YY 的 GCD#
题目大意
设 为所有质数构成的集合。求
组数据。数据范围:,。TL:4s。
题目分析
设 ,。
然后就和上面一样了...... 直接写结果吧。
由于 不是积性函数,我们需要用魔力筛计算 。然后就可以数论分块搞了。
时间复杂度为 。
代码
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=1e7+5;
int v[N],g[N];
vector<int>pr;
inline void init(){
const int n=N-5;
for(int i=2;i<=n;i++){
if(!v[i])pr.push_back(v[i]=i),g[i]=1;
for(int p:pr){
if(p>v[i]||p>n/i)break;
v[p*i]=p;
}
}
for(int p:pr){
for(int i=n/p;i>=1;i--)g[i*p]-=g[i];
}
for(int i=1;i<=n;i++)g[i]+=g[i-1];
}
int main(){
init();
int n,m,T;scanf("%d",&T);
ll ans;
while(T--){
scanf("%d%d",&n,&m);
if(n>m)swap(n,m);ans=0;
for(int l=1,r;l<=n;l=r+1){
r=min({n,n/(n/l),m/(m/l)});
ans+=(ll)(g[r]-g[l-1])*(n/l)*(m/l);
}
printf("%lld\n",ans);
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现