【学习笔记 / 长期更新】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 用
求解的一个问题 - 3.3.2 有理数取余:用
求解的另一个问题 - 3.3.3
例题
- 3.3.1 用
-
4 数论分块
- 4.1 算法
- 4.2 例题
- 4.3 用数论分块解决的一个问题
-
-
Reference
- Preface
0.1 前言
本文意为作者从
如果你看到有一些小标题没有内容,很正常,作者
同时本文用了大量
都看到这了不点个赞吗 /kel
0.2 常用符号
-
整除符号:
,表示 整除 。 -
取模符号:
,表示 除以 的余数。 -
互质符号:
,表示 与 互质。 -
最大公约数:
或 。 -
最小公倍数:
或 。 -
阶乘符号:
,表示 ,特别的, 。 -
求和符号:
,表示特定条件下几个数的和,例如: 表示 的和。 表示 里有几个数和它互质,即 。
-
求积符号:
,表示特定条件下几个数的积,例如: 表示 的阶乘,即 。 表示 的所有因数的乘积。
-
向上取整符号:
,表示大于等于 的最大的整数。 -
向下取整符号:
,表示小于等于 的最大的整数。 -
组合数:
。 -
整数集:
,表示所有的整数。 -
自然数集:
,表示所有的自然数。 -
实数集:
,表示所有的实数。 -
存在:
,表示至少存在一个 使得 的值为真。
0.3 快速幂
从熟悉的快速幂开始学起。
题意:给定
,求 。
朴素算法是
设
又由幂的性质可得,
于是
又有
- 例如:
由于取模并不影响乘法的运算,所以这种方法也可以求
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 整除
设
否则我们就称
性质:
。- 设
,则 。 - 设
,那么 。 - 设
,那么 。
对于整数
对于整数
对于整数
- 设
,那么当 遍历 的因数的时候, 也在遍历 的因数。 - 设
,那么当 遍历 的正因数的时候, 也在遍历 的正因数。
1.2 带余除法
设
这里的
其中
性质
- 连续
个整数除以 ,余数一定且正好取到 各一个,其中有且仅有一个正好被 整除。 - 任何一个整数除以
,余数一定为 其中一个。
1.3 同余
1.4 数论函数
1.4.1 积性函数
指的是如果
特殊的,如果
- 如欧拉函数
2 素数
定义:若整数
2.1 唯一分解定理
- 合数总能分解成几个素数的乘积。
形式化的,设
- 例如
2.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 判定素数
有时候,要判定的数的级别是 check_sqrt
就会 TLE,这时我们可以对其进行 Miller-Rabin 素数测试,来判断其是否为素数。
2.3.1 费马小定理(Fermat's Little Theorem)
有质数
,则对于正整数 ,有 。
考虑它的逆否命题,我们就有了一种判断素数的方法:
如果
,则 不是素数。
- 注意 FLT 的逆定理是不成立的,即对于正整数
,有 ,则 是质数,一个典型的例子是 数。
2.3.2 二次探测定理
对于上面的
如果
是一个素数, ,那么 的解只有两个: 。
证明:
于是有
2.3.3 Miller-Rabin
于是我们有了一种判定素数的方法:Miller-Rabin。
具体步骤:
- 如果
或 ,直接返回false
; - 把
化成 的形式,直到不能化为止,设最后的底数为 。 - 选择素数
,计算 。 - 不断运用二次探测定理,判断当
成立时,是否 或 ,如果不是即探测失败,返回false
。 - 如果满足条件,就判断
是否成立,如成立,则这一轮 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;
}
2.4 素数筛
通过
一个显然的做法是扫一遍区间,用根号做法或者 Miller-Rabin 判断每一个数是否是素数,但是
2.4.1 埃氏筛
由唯一分解定理(
于是我们可以得到这样一种筛法(埃拉托斯特尼筛法,简称埃氏筛):建立一个标记数组 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 欧拉筛 / 线性筛
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;
}
}
}
}
2.4.3 筛法的一些优化
- 空间优化:当空间限制很紧的时候,可以用
std::bitset
代替bool
数组节省空间,但是这会使程序效率降低,可能会 TLE(以空间换时间)。
2.5 分解质因数
给定一个数
2.5.1 朴素算法
根据唯一分解定理,一个合数总能被分解成几个素数的乘积,于是我们就有了一个类似根号时间内判定素数的程序,其时间复杂度也为
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 素数筛优化
我们知道,
代码和上文几乎一样,只是把
- 实际上,筛出素数也需要
的时间,所以如果 很大,还是得选择更优秀的算法,如下文介绍的 Pollard-Rho。
2.5.3 Pollard-Rho
2.6 欧拉函数
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 朴素求法
2.6.4.2 Pollard-Rho
2.6.5 筛法求欧拉函数
我们注意到,欧拉筛每次都会以它最小的素因子筛到这个数。设
,这也就是说, 中有 全部的素因子,于是
,这也就是说 互质,于是根据积性,有
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 最大公约数
定义正整数
对于如何快速地求出
- 欧几里得算法:对于正整数
,有 。
下面给出这个算法的证明过程(个人觉得 OI-Wiki 上的有些不严谨):
Proof
-
等式
:如果 ,有 。(1.1 整除) -
等式
: 。(1.1 整除) -
等式
: 。(1.2 带余除法) -
推论
: 。- 很显然
的公约数里有 共同的因子,然后 显然也有他们共同的因子,根据 唯一分解定理, 要么比 含有的因子数多,要么他们共同含有的因子 的幂次一定大于 (不然不是最大公约数),于是 。
- 很显然
接下来我们来推出
设
由 等式
于是根据 等式
由 推论
即
接下来我们来推出
设
由带余除法,
于是根据 等式
于是有
即
于是根据 等式
至此,欧几里得定理(或辗转相除法、
因为每次
常见的几种写法:
// 以下默认 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 最小公倍数
定义正整数
我们接下来来推一推
由 唯一分解定理,可得
因为
同理,
因为
也就是说,只要求出了两个数的
ll lcm(ll a,ll b){
return a/gcd(a,b)*b; // 防止 a*b 爆 ll,所以先 /gcd 再 *b
}
3.3 扩展欧几里得算法
扩展欧几里得算法(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 用 求解的一个问题
题意:给定
问题转化:
明显地,
引理:
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 有理数取余:用 求解的另一个问题
- 给定
,求 的值。
这个值被定义为
我们来分析一下。
因为同余有性质: 如果
慢着,我们先看上一个问题,他求解的是
于是有
等等,那无解情况呢?我们分情况讨论一下:
-
,即 为 的倍数: ,即 也为 的倍数,于是原同余式有无数解。 ,即 不为 的倍数,于是原同余式无解(因为等式左边 永远为 )。
-
,即 不为 的倍数:- 即
互质, ,于是根据 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 例题
4 数论分块
4.1 算法
给定
, 求 的值。
朴素算法是
有结论:对于
证明:设
容易证得,符合要求的区间约有
时间复杂度
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 例题
4.3 用数论分块解决的一个问题
以 P2261 [CQOI2007]余数求和 为例。
给出
,求 。
题目转化:由带余除法一章,我们知道
于是问题就变成了
其中
注意这题和上面那题不同。这题
以及
复杂度:
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;
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战