数论
yanmingqian · · 算法·理论
观前提示:本文为个人对于数论一部分知识点的整理,由于作者能力有限,可能会有错误或者不给出严格证明的情况,敬请谅解,欢迎提出修正,我会尽量修改。文中给出的代码大多为示意核心代码,可能会出现不严谨(如不开 long long)的情况,不保证能通过模板题或相关题目。可以配合 OI Wiki 或者其他资料食用,效果更佳。感谢阅读!
数论函数(算术函数)
数论函数(也称算数函数)指定义域为正整数的函数。数论函数也可以视作一个数列。
积性函数(乘性函数)
定义
若函数
性质
若
设正整数
若
例子
常数函数:
恒等函数:
除数函数:
幂函数:
单位函数:
以上积性函数常在狄利克雷卷积中用到。
欧拉函数
莫比乌斯函数
加性函数
定义
若函数
性质
设正整数
若
例子
所有质因子个数
相异质因子个数
所有质因子之和
相异质因子之和
扩展欧几里得(exgcd)
扩展欧几里得算法可以用于求解形如
推导过程略,见 OI Wiki,此处仅给出代码。
int exgcd(int a,int b,int &x,int &y){
if(!b){
x=1,y=0;return a;
}
int c=exgcd(b,a%b,y,x);
y-=(a/b)*x;
return c;
}
函数的返回值为
费马小定理
若
证明是不会的。
逆元
定义
如果
用处
求解
扩展欧几里得法
原式可转化为:
定理方程
因此
所以我们可以直接使用 exgcd 求解。如果要求最小的逆元,需在求完后加上一行 x=(x%b+b)%b
。
费马小定理法
由
若
所以
可以快速幂求解。
注意:费马小定理法只能在
线性求逆元
一个一个求太慢,考虑推式子然后递推。
(已知
如何求
显然
式子两边同乘
这样我们就可以递推了。
核心代码:
inv[1]=1;
for(int i=2;i<=n;i++){
inv[i]=(p-p/i)*inv[p%i]%p;
}
素数筛
埃氏筛
每次把该数的倍数(不包括自己)标记为合数,这样没有被标记过的就是素数。
code:
bool isprime[N];
long long prime[N];
void Eratosthenes(int n){ //埃氏筛
isprime[1]=1;
for(int i=2;i<=n;i++){
if(!isprime[i]){
prime[++prime[0]]=i;
for(long long j=1ll*i*i;j<=n;j+=i){
isprime[j]=1;
}
}
}
}
时间复杂度
线性筛(欧拉筛)
考虑优化埃氏筛。显然每个合数会被它的多个质因数重复标记,这样是不优的。让每个数只被其最小的质因子标记一次就可以做到
code:
bool isprime[N];
long long prime[N];
void Euler(int n){ //欧拉筛
isprime[1]=1;
for(int i=2;i<=n;i++){
if(!isprime[i]){
prime[++prime[0]]=i;
}
for(int j=1;j<=prime[0]&&i*prime[j]<=n;j++){
isprime[i*prime[j]]=1;
if(i%prime[j]==0){
break;
}
}
}
}
两份代码中,prime[0]
用来记录已经筛出的素数个数。
欧拉函数
定义
定义
其中
则
证明:
解释一下:第一步变换是根据性质三,第二步根据性质二与性质三,第三步注意到式子的前半部分就是
性质五
做题时经常用到的一个结论。疑似叫欧拉反演。
证明:\
假设我们有
更常用的是把
求欧拉函数
单个
利用性质四,枚举
code:
long long phi(int n){
long long ans=n;
for(int i=2;i*i<=n;i++){
if(n%i==0){
ans=ans/i*(i-1);
while(n%i==0){
n/=i;
}
}
}
if(n>1){
ans=ans/n*(n-1);
}
return ans;
}
时间复杂度
多个
如何求解
已知我们已经求得了
利用性质三不难证明。
由此我们可以在线性筛的基础上写出求欧拉函数的代码:
bool isprime[N];
long long prime[N],phi[N];
void Euler(int n){
isprime[1]=1;
phi[1]=1;
for(int i=2;i<=n;i++){
if(!isprime[i]){
prime[++prime[0]]=i;
phi[i]=i-1;
}
for(int j=1;j<=prime[0]&&i*prime[j]<=n;j++){
isprime[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]*phi[prime[j]];
}
}
}
}
莫比乌斯反演
莫比乌斯函数
设
如何用
打表不难发现,
其中
若式子成立,不难发现,若
因为
定义
也就是说,如果
性质
性质一
莫比乌斯函数
性质二
即
证明:
当
当
重要结论
由性质二,将
有关
求莫比乌斯函数
在线性筛的基础上修改,与欧拉函数类似。原理见定义与性质。
bool isprime[N];
long long prime[N],mu[N];
void Euler(int n){
isprime[1]=1;
mu[1]=1;
for(int i=2;i<=n;i++){
if(!isprime[i]){
prime[++prime[0]]=i;
mu[i]=-1;
}
for(int j=1;j<=prime[0]&&i*prime[j]<=n;j++){
isprime[i*prime[j]]=1;
if(i%prime[j]==0){
mu[i*prime[j]]=0;
break;
}
else{
mu[i*prime[j]]=-mu[i];
}
}
}
}
数论分块(整除分块)
用于计算含有下取整的和式,如
对于
对于小数据打表,不难发现,
以
18 9 6 4 3 3 2 2 2 1 1 1 1 1 1 1 1 1
那么我们可以想办法得出每一块中的值和每一块的长度,进而在更快时间内计算。
事实上,有如下结论:
结论
值
证明:
令
因此
所以满足条件的
知道了块的左右端点,也就是知道了块长,我们前缀和预处理
代码实现
for(int l=1,r;l<=n;l=r+1){
r=n/(n/l);
ans+=(sum[r]-sum[l-1])*(n/l); //sum[]是预处理的前缀和
}
时间复杂度是
用途
一个重要用途是和莫比乌斯反演结合以进一步降低复杂度。这里给出一道两者结合的经典题目:[POI 2007] ZAP-Queries,大致思路是利用莫比乌斯反演中的重要结论得出和式,再利用整除分块在最后运算时加速。
狄利克雷卷积(Dirichlet 卷积)
定义
对于两个数论函数
上式可简记为:
性质
类似于普通乘法,有以下性质:
交换律
结合律
分配律
等式的性质
单位元单位函数
逆元对于任一满足
几个等式
-
\mu \times I=\varepsilon -
\varphi \times I=id -
\mu \times id=\varphi
重要结论
srds,我不知道这俩结论为啥重要,也不知道咋证,所以只给个结论很合理吧。
结论一
两个积性函数的 Dirichlet 卷积也是积性函数。
结论二
积性函数的逆元也是积性函数。
证明好像可以归纳法???
杜教筛
推导
已知一个积性函数
我们设
注意到,
最后这个就是杜教筛的核心式子。
因此如果我们能找到一个合适的积性函数
这么做的时间复杂度是
应用
\varphi 的前缀和
我们知道
int get_S_phi(int n){
if(n<=N){
return S_phi_init[n];
}
if(S_phi[n]){
return S_phi[n];
}
int ans=n*(n+1)/2;
for(int l=2,r;l<=n;l=r+1){
r=n/(n/l);
ans-=(r-l+1)*get_S_phi(n/l);
}
return S_phi[n]=ans; //记忆化
}
\mu 的前缀和
同理,由
int get_S_mu(int n){
if(n<=N){
return S_mu_init[n];
}
if(S_mu[n]){
return S_mu[n];
}
int ans=1;
for(int l=2,r;l<=n;l=r+1){
r=n/(n/l);
ans-=(r-l+1)*get_S_mu(n/l);
}
return S_mu[n]=ans;
}
这里一并给出 P4213 【模板】杜教筛 的全部代码:
#include<iostream>
#include<unordered_map>
using namespace std;
#define int long long
const int N=3e6;
bool isprime[N+10];
int prime[N+10],phi[N+10],mu[N+10];
void Euler(int n){
isprime[1]=1;
phi[1]=mu[1]=1;
for(int i=2;i<=n;i++){
if(!isprime[i]){
prime[++prime[0]]=i;
phi[i]=i-1;
mu[i]=-1;
}
for(int j=1;j<=prime[0]&&i*prime[j]<=n;j++){
isprime[i*prime[j]]=1;
if(i%prime[j]==0){
phi[i*prime[j]]=phi[i]*prime[j];
mu[i*prime[j]]=0;
break;
}
else{
phi[i*prime[j]]=phi[i]*phi[prime[j]];
mu[i*prime[j]]=-mu[i];
}
}
}
}
int S_phi_init[N+10],S_mu_init[N+10];
void init(){
Euler(N);
for(int i=1;i<=N;i++){
S_phi_init[i]=S_phi_init[i-1]+phi[i];
S_mu_init[i]=S_mu_init[i-1]+mu[i];
}
}
unordered_map<int,int> S_phi,S_mu;
int get_S_phi(int n){
if(n<=N){
return S_phi_init[n];
}
if(S_phi[n]){
return S_phi[n];
}
int ans=n*(n+1)/2;
for(int l=2,r;l<=n;l=r+1){
r=n/(n/l);
ans-=(r-l+1)*get_S_phi(n/l);
}
return S_phi[n]=ans;
}
int get_S_mu(int n){
if(n<=N){
return S_mu_init[n];
}
if(S_mu[n]){
return S_mu[n];
}
int ans=1;
for(int l=2,r;l<=n;l=r+1){
r=n/(n/l);
ans-=(r-l+1)*get_S_mu(n/l);
}
return S_mu[n]=ans;
}
signed main(){
init();
int T;
cin>>T;
while(T--){
int n;
cin>>n;
cout<<get_S_phi(n)<<" "<<get_S_mu(n)<<"\n";
}
return 0;
}
卢卡斯定理 / Lucas 定理
定义
定义
则有
其中
代码实现
递归即可。
int Lucas(int n,int m,int p){
if(!m){
return 1;
}
return Lucas(n/p,m/p,p)*C(n%p,m%p,p)%p;
}
扩展欧拉定理
定义
证明(不严谨)
不难想到,
代码实现
我直接略略略略略。
仅给出示意核心代码。
cout<<qpow(a,(b<phi(p))?b:(b%phi(p)+phi(p)),p);
中国剩余定理(CRT)
定义
中国剩余定理可以用于求解如下形式关于
过程
- 计算所有模数的积
M ; - 遍历每个方程,对于第
i 个方程:\ a. 计算m\_now=\frac{M}{m_i} ;\ b. 计算m\_now 在模m_i 意义下的逆元m\_now^{-1} (使用 exgcd 算法);\ c. 计算c_i=m\_now\times m\_now^{-1} ; - 方程组在模
M 意义下的唯一解为:x=\sum^k_{i=1} a_ic_i \pmod M 。
代码实现
int CRT(){
int ans=0;
for(int i=1;i<=n;i++){
int m_now=M/m[i],x,y;
exgcd(m_now,m[i],x,y);
ans=((ans+m_now*x*a[i])%M+M)%M;
}
return ans;
}