【学习笔记 / 长期更新】OI 中的数论
目录
-
Preface
- 0.1 前言
- 0.2 常用符号
- 0.3 快速幂
-
数论
-
1 数论基础
- 1.1 整除
- 1.2 带余除法
- 1.3 同余
- 1.4 数论函数
-
2 素数
- 2.1 唯一分解定理
- 2.2 判定素数
- 2.3 Miller-Rabin 判定素数
- 2.3.1 费马小定理
- 2.3.2 二次探测定理
- 2.3.3 Miller-Rabin
- 2.4 素数筛
- 2.4.1 埃氏筛
- 2.4.2 欧拉筛 / 线性筛
- 2.4.3 筛法的一些优化
- 2.5 分解质因数
- 2.5.1 朴素算法
- 2.5.2 素数筛优化
- 2.5.3 Pollard-Rho
- 2.6 欧拉函数
- 2.6.1 性质
- 2.6.2 欧拉函数
- 2.6.3 引理
- 2.6.4 求一个数的欧拉函数
- 2.6.4.1 朴素求法
- 2.6.4.2 Pollard-Rho
- 2.6.5 筛法求欧拉函数
-
3 最大公约数
- 3.1 最大公约数
- 3.2 最小公倍数
- 3.3 扩展欧几里得算法
- 3.3.1 用
\text{exgcd} 求解的一个问题 - 3.3.2 有理数取余:用
\text{exgcd} 求解的另一个问题 - 3.3.3
\text{exgcd} 例题
- 3.3.1 用
-
4 数论分块
- 4.1 算法
- 4.2 例题
- 4.3 用数论分块解决的一个问题
-
-
Reference
- Preface
0.1 前言
本文意为作者从
如果你看到有一些小标题没有内容,很正常,作者
同时本文用了大量
都看到这了不点个赞吗 /kel
0.2 常用符号
-
整除符号:
x \mid y ,表示x 整除y 。 -
取模符号:
x \bmod{y} ,表示x 除以y 的余数。 -
互质符号:
x \bot y ,表示x 与y 互质。 -
最大公约数:
\gcd(x,y) 或(x,y) 。 -
最小公倍数:
\operatorname{lcm}(x,y) 或[x,y] 。 -
阶乘符号:
n! ,表示1 \times 2 \times 3 \times \cdots \times n ,特别的,0!=1 。 -
求和符号:
\sum ,表示特定条件下几个数的和,例如: -
-
求积符号:
\prod ,表示特定条件下几个数的积,例如: -
-
向上取整符号:
\lceil x \rceil ,表示大于等于x 的最大的整数。 -
向下取整符号:
\lfloor x \rfloor ,表示小于等于x 的最大的整数。 -
组合数:
\binom{x}{y} 。 -
整数集:
\mathbf{Z} ,表示所有的整数。 -
自然数集:
\mathbf{N} ,表示所有的自然数。 -
实数集:
\mathbf{R} ,表示所有的实数。 -
存在:
\exists x:P(x) ,表示至少存在一个x 使得P(x) 的值为真。
0.3 快速幂 \Theta(\log n)
从熟悉的快速幂开始学起。
题意:给定
a,b ,求a^b 。
朴素算法是
设
又由幂的性质可得,
于是
又有
- 例如:
由于取模并不影响乘法的运算,所以这种方法也可以求
Sample Code
// 0.3 qpow
template<class T = long long> T qpow(T a, T b,const T mod = b - 2){
T ans = 1, base = a;
while(b){
if(b & 1) ans = ans * base % mod;
base = base * base % mod;
b >>= 1;
}
return ans;
}
- 数论
1 数论基础
1.1 整除
设
否则我们就称
性质:
-
-
a \mid b \land b \mid c \implies a \mid c -
a\mid b\land a\mid c\iff\forall x,y\in\mathbf{Z}, a\mid(xb+yc) -
a\mid b\land b\mid a\implies b=\pm a - 设
m \neq 0 ,则a\mid b\iff ma\mid mb 。 - 设
b \ne 0 ,那么a \mid b \implies |a| \le |b| 。 - 设
a \ne 0,b=qa+c ,那么a \mid b \iff a \mid c 。
以上性质的证明。
对于整数
对于整数
对于整数
- 设
b\ne 0 ,那么当d 遍历b 的因数的时候,\frac{b}{d} 也在遍历b 的因数。 - 设
b> 0 ,那么当d 遍历b 的正因数的时候,\frac{b}{d} 也在遍历b 的正因数。
1.2 带余除法
设
这里的
其中
性质
- 连续
a 个整数除以a ,余数一定且正好取到0 \sim a-1 各一个,其中有且仅有一个正好被a 整除。 - 任何一个整数除以
a ,余数一定为0 \sim a-1 其中一个。
1.3 同余
1.4 数论函数
1.4.1 积性函数
指的是如果
特殊的,如果
- 如欧拉函数
\varphi(a\times b)=\varphi(a) \times \varphi(b)\ \{\gcd(a,b)=1\}
2 素数
定义:若整数
通过素数的性质可知,对于素数
Sample Code
// 1.2.2 prime check O(n)
bool check_linear(int x){
for(int i = 2; i < x; i++){
if(x % i == 0) return false;
}
return true;
}
显然,当
注意到
也就是说,当
Sample Code
// 1.2.2 prime check O(sqrt(n))
bool check_sqrt(int x){
for(int i = 2; i * i <= x; i++){ //注意这里是 <=x,不然可能把一些完全平方数放过去
if(x % i == 0) return false;
}
return true;
}
2.3 Miller-Rabin 判定素数 \Theta(k \log^3n)
有时候,要判定的数的级别是 check_sqrt 就会 TLE,这时我们可以对其进行 Miller-Rabin 素数测试,来判断其是否为素数。
2.3.1 费马小定理(Fermat's Little Theorem)
有质数
p ,则对于正整数a \nmid p ,有a^{p-1} \equiv 1\pmod{p} 。
考虑它的逆否命题,我们就有了一种判断素数的方法:
如果
a^{p-1} \not \equiv 1 \pmod{p} ,则p 不是素数。
- 注意 FLT 的逆定理是不成立的,即对于正整数
1 \le a <p ,有a^{p-1} \equiv 1\pmod{p} ,则p 是质数,一个典型的例子是\text{Carmichael} 数。
2.3.2 二次探测定理
对于上面的
如果
p 是一个素数,0 < x < p ,那么x^2 \equiv 1 \pmod{p} 的解只有两个:x_1=1,x_2=p-1 。
证明:
于是有
2.3.3 Miller-Rabin
于是我们有了一种判定素数的方法:Miller-Rabin。
具体步骤:
- 如果
n=1 或n \bmod{2}=0 ,直接返回false; - 把
a^{n-1} 化成(a^{\frac{n-1}{2}})^2,(a^{\frac{n-1}{4}})^{2^2} 的形式,直到不能化为止,设最后的底数为a^u 。 - 选择素数
a ,计算a^u \bmod n 。 - 不断运用二次探测定理,判断当
x^2 \equiv 1 \pmod{p} 成立时,是否x=1 或x=n-1 ,如果不是即探测失败,返回false。 - 如果满足条件,就判断
a^{n-1} \equiv1\pmod{p} 是否成立,如成立,则这一轮 Miller-Rabin 检测结束。
一般来说素数 long long 范围内选择
时间复杂度:
Sample Code
// 1.2.3.3 Miller-Rabin
typedef long long ll;
typedef unsigned long long ull;
typedef long double ld;
ll mul(ll a,ll b,ll mod){
// 快速乘,原理是用 ull 的溢出解决 ll 的溢出
ll c=(ld)a/mod*b;
ll res=(ull)a*b-(ull)c*mod;
return (res+mod)%mod;
}
ll qpow(ll a,ll b,const ll p){
// 快速幂
ll ans=1ll,base=a;
while(b){
if(b&1) ans=mul(ans,base,p);
base=mul(base,base,p);
b>>=1;
}
return ans;
}
ll ud[]={2,3,5,7,11,13,17,19,23}; // 素数集
bool MRtest(ll n){
if(n%2==0 || n<3) return n==2; // 特判
ll u=n-1,t=0;
while(u%2==0) u>>=1,++t; // 分解为 a^u
ll x,pre;
for(auto p:ud){ // 选择素数
if(p==n) return true; // 特判
x=qpow(p,u,n);
pre=x;
for(int i=1;i<=t;i++){
x=mul(x,x,n);
if(x==1 && pre!=1 && pre!=n-1) return false;
pre=x; // 二次探测
}
if(x!=1) return false; // 费马小定理
}
return true;
}
- 应用:https://www.luogu.com.cn/problem/U82118
2.4 素数筛
通过
一个显然的做法是扫一遍区间,用根号做法或者 Miller-Rabin 判断每一个数是否是素数,但是
2.4.1 埃氏筛 \Theta(n \log \log n)
由唯一分解定理(
于是我们可以得到这样一种筛法(埃拉托斯特尼筛法,简称埃氏筛):建立一个标记数组 vis,每次从小到大选择未被标记的数,把它的倍数都标记为合数,这样在结束后未被标记的数就是素数。
时间复杂度为
Sample Code
// 1.2.4.1 Eratosthenes sieve
bool vis[1000005]; //标记此数是不是素数
vector<int> prime; //素数集
void Eratosthenes_sieve(int n){
vis[1] = 1;
for(int i = 2; i <= n; i++){
if(vis[i] == 0){
prime.push_back(i);
for(int j = i + i; j <= n; j += i){
vis[j] = 1;
}
}
}
}
2.4.2 欧拉筛 / 线性筛 \Theta(n)
Eratosthenes sieve 已经足够快了,但是碰到一些
我们注意到,在埃氏筛中,
Sample Code
// 1.2.4.2 Euler sieve
bool vis[1000005];
vector<int> prime;
void Euler_sieve(int n){
vis[1] = 1;
for(int i = 2; i <= n; i++){
if(!vis[i]) prime.push_back(i);
for(int j = 0; (size_t)j < prime.size(); j++){
int it = prime.at(j);
if(1ll * it * i > n) break;
vis[1ll * it * i] = 1;
if(i % it == 0){
// i % pri[j] == 0
// 换言之,i 之前被 pri[j] 筛过了
// 由于 pri 里面质数是从小到大的,所以 i 乘上其他的质数的结果一定也是
// pri[j] 的倍数 它们都被筛过了,就不需要再筛了,所以这里直接 break
// 掉就好了
// From OI-Wiki
break;
}
}
}
}
- 应用:https://www.luogu.com.cn/problem/P3383
2.4.3 筛法的一些优化
- 空间优化:当空间限制很紧的时候,可以用
std::bitset代替bool数组节省空间,但是这会使程序效率降低,可能会 TLE(以空间换时间)。
2.5 分解质因数
给定一个数
2.5.1 朴素算法 \Theta(\sqrt{n})
根据唯一分解定理,一个合数总能被分解成几个素数的乘积,于是我们就有了一个类似根号时间内判定素数的程序,其时间复杂度也为
Sample Code
vector<pair<int,int> > factor(ll x){ // 返回一个 pair<int,int> 类型的 vector
vector<pair<int,int> > v; // first 存储素因子,second 存储素因子的次数
for(ll i=2;i*i<=x;i++){
if(x%i==0){
int t=0;
while(x%i==0) t++,x/=i;
v.push_back(make_pair(i,t));
}
}
if(x>1) v.push_back(make_pair(x,1)); // 如果 x 本身就是个素数
return v;
}
2.5.2 素数筛优化 \Theta(\sqrt{\frac{n}{\ln n}})
我们知道,
代码和上文几乎一样,只是把
- 实际上,筛出素数也需要
\Theta(n) 的时间,所以如果n 很大,还是得选择更优秀的算法,如下文介绍的 Pollard-Rho。
2.5.3 Pollard-Rho \Theta(n^{\frac{1}{4}})
2.6 欧拉函数 \varphi(n)
2.6.1 性质
首先有欧拉函数是积性函数,也就是说,如果
由素数定义可知,如果
2.6.2 欧拉函数(Euler's totient function)
凭借 2.6.1 和 2.1 唯一分解定理
于是我们就有了欧拉函数的计算方法,设合数
我们来分析一下这个公式是怎么来的。
2.6.3 引理
设
这很好证明,
于是,根据欧拉函数的积性和唯一分解定理可知
2.6.4 求一个数的欧拉函数
2.6.4.1 朴素求法 \Theta(\sqrt n)
2.6.4.2 Pollard-Rho \Theta(n ^{\frac{1}{4}})
2.6.5 筛法求欧拉函数 \Theta(n)
我们注意到,欧拉筛每次都会以它最小的素因子筛到这个数。设
Sample Code
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000000 + 5;
int pri[MAXN],phi[MAXN],cnt;
bool is_prime[MAXN];
void get_euler(const int N){
for(int i=1;i<=N;i++) is_prime[i]=1;
is_prime[1]=0;
phi[1]=1;
cnt=0;
for(int i=1;i<=N;i++){
if(is_prime[i]){
pri[++cnt]=i;
phi[i]=i-1; //i为素数,phi[i]=i-1
}
for(int j=1;j<=cnt;j++){
if(i*pri[j]>N) break;
is_prime[i*pri[j]]=0;
if(i%pri[j]) // 注意分辨 phi 和 pri
phi[i*pri[j]]=phi[i]*phi[pri[j]]; // 情况1
else{
phi[i*pri[j]]=phi[i]*pri[j]; // 情况2
break;
}
}
}
}
int n,T;
int main(){
get_euler(MAXN-5);
cin>>T;
while(T--){
cin>>n;
cout<<phi[n]<<'\n';
}
}
3 最大公约数
3.1 最大公约数 \gcd(a,b)
定义正整数
对于如何快速地求出
- 欧几里得算法:对于正整数
a,b ,有\gcd(a,b)=\gcd(b,a \bmod{b}) 。
下面给出这个算法的证明过程(个人觉得 OI-Wiki 上的有些不严谨):
Proof
-
等式
1 :如果a \mid b \land b \mid a ,有a = \pm b 。(1.1 整除) -
等式
2 :a\mid b\land a\mid c\iff\forall x,y\in\mathbf{Z}, a\mid(xb+yc) 。(1.1 整除) -
等式
3 :a \in \mathbf{Z}, n \in \mathbf{N}^{+},a \bmod n = a-n\left\lfloor\dfrac{a}{n}\right\rfloor 。(1.2 带余除法) -
推论
1 :d \mid a \land d \mid b \implies d \mid \gcd(a,b) 。- 很显然
a,b 的公约数里有a,b 共同的因子,然后\gcd(a,b) 显然也有他们共同的因子,根据 唯一分解定理,\gcd(a,b) 要么比d 含有的因子数多,要么他们共同含有的因子\gcd(a,b) 的幂次一定大于d (不然不是最大公约数),于是d \mid \gcd(a,b) 。
- 很显然
接下来我们来推出
设
由 等式
于是根据 等式
由 推论
即
接下来我们来推出
设
由带余除法,
于是根据 等式
于是有
即
于是根据 等式
至此,欧几里得定理(或辗转相除法、
因为每次
常见的几种写法:
// 以下默认 typedef long long ll
ll gcd(ll a,ll b){ // 非递归版
int tmp;
while(b!=0){
tmp=b;
b=a%b;
a=tmp;
}
return a;
}
ll gcd(ll a,ll b){ // 递归版
if(b==0) return a;
return gcd(b,a%b);
}
ll gcd(ll a,ll b){ // 递归版简化
return b==0?a:gcd(b,a%b);
}
ll gcd(ll a,ll b){ // 位运算版
while(b){
a%=b;
b^=a;
a^=b;
b^=a;
}
return a;
}
ll gcd(ll a,ll b){ // 位运算简化
while(b^=a^=b^=a%=b);
return a;
}
3.2 最小公倍数 \operatorname{lcm}(a,b)
定义正整数
我们接下来来推一推
由 唯一分解定理,可得
因为
同理,
因为
也就是说,只要求出了两个数的
ll lcm(ll a,ll b){
return a/gcd(a,b)*b; // 防止 a*b 爆 ll,所以先 /gcd 再 *b
}
3.3 扩展欧几里得算法 \text{exgcd}
扩展欧几里得算法(Extended Euclidean algorithm),简写为
接下来我们来分析一下这个算法:
首先,当
设
于是根据 欧几里得定理,显然有
于是
用
ll exgcd(ll a,ll b,ll &x,ll &y){
if(b==0){
x=1; y=0;
return a;
}
ll d=exgcd(b,a%b,x,y);
ll t=x;
x=y;
y=t-(a/b)*y;
return d;
}
3.3.1 用 \text{exgcd} 求解的一个问题
题意:给定
问题转化:
明显地,
引理:
Proof:因为
于是再联系到题目上,就有
这也就是说,
最小正整数解
因为
这也就是说,如果
这在 C++ 的运算中,用 x = (x % b + b) % b 即可解决,后面的 + b) % b 是为了让
Sample Code
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
ll a,b,x,y;
void exgcd(ll a,ll b,ll &x,ll &y){
if(b==0){
x=1; y=0;
return;
}
exgcd(b,a%b,x,y);
ll tmp=x;
x=y;
y=tmp-(a/b)*y;
return;
}
int main(){
cin>>a>>b;
exgcd(a,b,x,y);
x=(x%b+b)%b;
cout<<x<<'\n';
return 0;
}
3.3.2 有理数取余:用 \text{exgcd} 求解的另一个问题
- 给定
c=\dfrac{a}{b} ,求c \bmod p 的值。
这个值被定义为
我们来分析一下。
因为同余有性质: 如果
慢着,我们先看上一个问题,他求解的是
于是有
等等,那无解情况呢?我们分情况讨论一下:
-
- $a \bmod p = 0$,即 $a$ 也为 $p$ 的倍数,于是原同余式有无数解。 - $a \bmod p \ne 0$,即 $a$ 不为 $p$ 的倍数,于是原同余式无解(因为等式左边 $\mod p$ 永远为 $0$)。 -
- 即 $b,p$ 互质,$\gcd(b,p)=1$,于是根据 **3.3.1** 中的一个结论,原方程必定有解。
总结:如果
Sample Code
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int p = 19260817;
inline ll read(){
char ch=getchar(); ll x(0),f(0);
for(; !isdigit(ch); ch=getchar()) f|=(ch=='-');
for(; isdigit(ch); ch=getchar()) x=((x<<1)+(x<<3)+(ch^48))%p;
return f?-x:x;
}
ll exgcd(ll a,ll b,ll &x,ll &y){
if(b==0){
x=1; y=0;
return a;
}
ll d=exgcd(b,a%b,x,y);
ll t=x;
x=y;
y=t-(a/b)*y;
return d;
}
ll a,b,x,y;
int main(){
a=read(); b=read();
if(b==0)
return puts("Angry!")&0;
exgcd(b,p,x,y);
ll ans=(a*x%p+p)%p;
printf("%lld\n",ans);
}
3.3.3 \text{exgcd} 例题
- https://www.luogu.com.cn/problem/P1082
- https://www.luogu.com.cn/problem/P5656
4 数论分块
4.1 算法
给定
n\in \mathbf N^{+} , 求\sum^n_{i=1} \left\lfloor\dfrac{n}{i}\right\rfloor 的值。
朴素算法是
有结论:对于
证明:设
容易证得,符合要求的区间约有
时间复杂度
Sample Code
ll H(int n){
ll res = 0;
int l = 1, r;
while(l <= n){
r = n / (n / l);
res += 1ll * (r - l + 1) * (n / l);
l = r + 1;
}
return res;
}
4.2 例题
- https://www.luogu.com.cn/problem/UVA11526
- https://www.luogu.com.cn/problem/P2261
4.3 用数论分块解决的一个问题
以 P2261 [CQOI2007]余数求和 为例。
给出
n,k ,求\sum^n_{i=1} k \bmod i 。
题目转化:由带余除法一章,我们知道
于是问题就变成了
其中
注意这题和上面那题不同。这题
以及
复杂度:
ll H(ll n, ll k){
ll ans = k * n;
int l = 1, r;
while(l <= n){
if(k / l != 0) r = k / (k / l);
else r = n;
chkmin(r, n);
ans -= 1ll * (l + r) * (r - l + 1) / 2 * (k / l);
l = r + 1;
}
return ans;
}
- Reference
-
https://oi-wiki.org/math/
-
https://zhuanlan.zhihu.com/p/87611586
-
https://www.cnblogs.com/zwfymqz/p/8150969.html
-
https://zhuanlan.zhihu.com/p/346479426
-
https://www.luogu.com.cn/blog/cicos/solution-p1082