【算法竞赛中的数学】
1. 《数论》
数论是纯粹数学的分⽀,主要研究的是整数的性质,被称为"最纯"的数学领域。
数论中有很多描述巨简单,但是证明很困难的理论。因此,在讲解数论相关的内容时,采取的⽅式是 先使⽤,后证明。
2. 《组合数学》
组合数学,在总体上是⼀⻔研究可数或离散对象的科学。它可分为⼴义上的和狭义上的两种层⾯,若 是前者 (⼴义的组合数学) ,其相当于离散数学,⽽后者 (狭义的组合数学) 则是组合计数、图论、代数 结构、数理逻辑等的总称,但这只是不同学者在称谓上的区别。
竞赛中的组合数学的主要内容就是组合计数,容斥原理。
3. 《线性代数》
线性代数是关于向量空间和线性映射的⼀个数学分⽀。主要研究向量,向量空间,矩阵理论,线性变 换以及有线维线性⽅程组等等。
竞赛中常⽤到的就是矩阵乘法以及⾼斯消元。
4. 《博弈论》
博弈论,⼜译为对策论,研究游戏或者博弈内的相互作⽤,是研究具有⽃争或竞争性质现象的数学理 论和⽅法。也是运筹学的⼀个重要学科。
5. 《概率论》
概率论是研究概率、随机性及不确定性等现象的数学分⽀。 竞赛中有关概率论的题⽬⼤多出现在动态规划中,会在《算法提⾼篇》重点讲解。
6. 《计算⼏何》
计算⼏何是⼏何外形信息的计算机表⽰、分析和综合。
【⼩要求】
• 学习数学千万不要只是去听,⼀定要把纸和笔拿出来,把课上的例⼦以及算法流程复刻⼀遍。
1. 最⼤公约数和最⼩公倍数
【约数和倍数】
• 如果 a除以 b没有余数,那么 a就是 b的倍数, b就是 a的约数,记作 b∣ a。 约数,也称因数。
【最⼤公约数和最⼩公倍数】
最⼤公约数 Greatest Common Divisor,常缩写为 gcd。
• ⼀组整数的公约数,是指同时是这组数中每⼀个数的约数的数。
• ⼀组整数的最⼤公约数,是指所有公约数⾥⾯最⼤的⼀个。
最⼩公倍数 Least Common Multiple,常缩写为 lcm。
• ⼀组整数的公倍数,是指同时是这组数中每⼀个数的倍数的数。
• ⼀组整数的最⼩公倍数,是指所有正的公倍数⾥⾯,最⼩的⼀个数。
求两个数的 gcd 与 lcm 时,有如下性质:
• 对于两个数 a 和 b , gcd ( a , b ) × lcm ( a , b ) = a × b 。也就是 最⼤公约数乘以最⼩公倍数等于两
个数的乘积。
因此,⼀般先求最⼤公约数,然后⽤这个性质求最⼩公倍数。
【欧⼏⾥得算法】
欧⼏⾥得算法也称辗转相除法,可以求出两个整数的最⼤公约数。
算法流程:
设 a> b:
• 如果 b是 a的约数,那么 b就是两者的最⼤公约数;
• 如果 b不是 a的约数,那么 gcd(a, b) = gcd(b, amod b)
因为 amod b会不断减⼩,因此可以⽤递归进⾏求解。
代码实现:
cpp
LL gcd(LL a, LL b)
{
if(!b) return a; // 如果 b 等于 0,说明 a 就是最⼤公约数
return gcd(b, a % b);
}
时间复杂度:
求 gcd ( a , b ) 会遇到两种情况:
- a < b ,则 gcd ( a , b ) = gcd ( b , a )
- a > b ,则 gcd ( a , b ) = gcd ( b , a mod b )
第⼆种情况会让 a ⾄少折半,因此最多执⾏ log n 次。
第⼀种情况不会多于第⼆种,因此时间复杂度为 O (log n ) 。

1.1 最⼤公约数
题⽬来源: 洛⾕
题⽬链接: B3736 [信息与未来 2018] 最⼤公约数
难度系数: ★
题目描述
输入三个正整数 x,y,z,求它们的最大公约数(Greatest Common Divisor)g:最大的正整数 g≥1,满足 x,y,z 都是 g 的倍数,即 (xmodg)=(ymodg)=(zmodg)=0。
输入格式
输入一行三个正整数 x,y,z。
输出格式
输出一行一个整数 g,表示 x,y,z 的最大公约数。
输入输出样例
输入 #1复制
12 34 56
输出 #1复制
2
输入 #2复制
28 70 28
输出 #2复制
14
说明/提示
样例解释
样例 1
12=2×6,34=2×17,56=2×28,g=2。
样例 2
28=14×2,70=14×5,28=14×2,g=14。
数据规模
所有数据满足 1≤x,y,z≤106。
本题原始满分为 15pts。
【解法】
三个数的最⼤公约数,先求其中两个的 gcd,再与第三个求 gcd。
【参考代码】
cpp
#include <iostream>
using namespace std;
int gcd(int a, int b)
{
return b == 0 ? a : gcd(b, a % b);
}
int main()
{
int x, y, z; cin >> x >> y >> z;
cout << gcd(gcd(x, y), z) << endl;
return 0;
}
1.2 ⼩红的 gcd
题⽬来源: ⽜客
题⽬链接: ⼩红的 gcd
难度系数: ★
链接: https://ac.nowcoder.com/acm/problem/275615
来源:牛客网
题号:NC275615
时间限制:C/C++/Rust/Pascal 1秒,其他语言2秒
空间限制:C/C++/Rust/Pascal 256 M,其他语言512 M
64bit IO Format: %lld
题目描述
给两个正整数a,ba,ba,b,输出他们的最大公约数 gcd(a,b)\gcd(a, b)gcd(a,b)。
输入描述:
第一行一个正整数 aaa。
第二行一个正整数 bbb。
lenlenlen表示aaa的十进制位数,1≤len≤1061\leq len \leq 10^61≤len≤106。
1≤b≤1091\leq b \leq 10^91≤b≤109。
输出描述:
输出一个整数,表示gcd(a,b)\gcd(a, b)gcd(a,b)。
示例1
输入
复制12345678 12
12345678
12
输出
复制6
6
【解法】
先将⼤数取模,然后再代⼊公式计算。
【秦九韶算法】
秦九韶算法是⼀种将⼀元 次多项式的求值问题转化为 个⼀次式的算法。其⼤ 简化了计算过
程,即使在现代,利⽤计算机解决多项式的求值问题时,秦九韶算法依然是最优的算法。
⼀个 n次多项式:
f(x) = *anxn*+ a**n−1x**n−1 + a**n−2x**n−2 + ... + a1x1 + a0x0**
可以改写成:
f(x) = (*anxn*−1 + a**n−1x**n−2 + a**n−2x**n−3 + ... + a1 )x+ a0= ((anx**n−2 + a**n−1x**n−3 + a**n−2x**n−4 + ... + a2 )x+ a1 )x+ a0**
.
.
.
= (..((*anx*+ *an*−1 )x+ a**n−2 )x+ ... + a2 )x+ a1 )x+ a0**
例如:对于⼀个整数 987654321 ,可以拆成:
(((((((9 × 10 + 8) × 10 + 7) × 10 + 6) × 10 + 5) × 10 + 4) × 10 + 3) × 10 + 2) × 10 + 1
这样对于⾼精度的数取模,就可以分阶段取模。
【参考代码】
cpp
#include <iostream>
using namespace std;
string a;
int b;
int gcd(int a, int b)
{
return b == 0 ? a : gcd(b, a % b);
}
int calc()
{
long long t = 0;
for(auto ch : a)
{
t = t * 10 + ch - '0';
t %= b;
}
return t;
}
int main()
{
cin >> a >> b;
cout << gcd(b, calc()) << endl;
return 0;
}
2. 质数的判定
【质数和合数】
• ⼀个⼤于1 的⾃然数,除了1 和它⾃⾝外,不能被其他⾃然数整除的数叫做质数;否则称为合
数。其中,质数⼜称素数。
规定 1 既不是质数也不是合数。
试除法判断质数
• 对于⼀个数 x ,根据定义,可以从 [2, x − 1] ⼀个⼀个尝试,判断 x 能否被整除。
但是,没有必要每⼀个都去判断。因为a 如果是x 的约数,那么x/a 也是 x的约数。因此,我们
仅需判断较⼩的 a是否是 x的约数,没有必要再去看看 。那么,仅需枚举到sqrt(x) 即可到 。
代码实现:
cpp
bool isprime(int x)
{
if(x <= 1) return false; // ⼩于等于 1 的数不考虑
// 试除法判断是否是质数 - 只需枚举到 sqrt(x)
for(int i = 2; i <= x / i; i++) // 防溢出的写法
{
if(x % i == 0) return false;
}
return true;
}
时间复杂度:
枚举到 ,因此时间复杂度为 O (
) 。
2.1 质数筛
题⽬来源: 洛⾕
题⽬链接: P5736 【深基7.例2】质数筛
难度系数: ★
题目描述
输入 n 个不大于 105 的正整数。要求全部储存在数组中,去除掉不是质数的数字,依次输出剩余的质数。
输入格式
第一行输入一个正整数 n,表示整数个数。
第二行输入 n 个正整数 ai,以空格隔开。
输出格式
输出一行,依次输出 ai 中剩余的质数,以空格隔开。
输入输出样例
输入 #1复制
5
3 4 5 6 7
输出 #1复制
3 5 7
说明/提示
数据保证,1≤n≤100,1≤ai≤105。
【解法】
读⼀个判断⼀个即可。
【参考代码】
cpp
#include <iostream>
using namespace std;
bool isprime(int x)
{
if(x <= 1) return false;
for(int i = 2; i <= x / i; i++)
{
if(x % i == 0) return false;
}
return true;
}
int main()
{
int n; cin >> n;
for(int i = 1; i <= n; i++)
{
int x; cin >> x;
if(isprime(x)) cout << x << " ";
}
return 0;
}
3. 筛质数
【引⼊】
上⼀个专题学习了如何判断⼀个数是否是质数,如果此时想知道[1, n ] 中有多少个素数呢?或者是
1, *n* \]中的素数⾥⾯,第 k个素数是多少?
• ⼀个⾃然的想法就是从 2 开始,依次向后对每⼀个⾃然数进⾏⼀次质数检验。
但是这种解法相对暴⼒,我们这⾥介绍两种⽅法,能够快速地将 \[1, *n* \] 中的素数全部记录下来。
*** ** * ** ***
### 【第⼀种筛法:埃⽒筛法】
算法思想:
• 对于任意⼀个⼤于 1 的正整数 ,那么它的 *k* ( *k* \> 1) 倍就是合数。
因此,如果我们从⼩到⼤考虑每个数,然后同时把当前这个数的所有倍数记为合数,没有被标记的数 就是素数。
⼩优化:
• 找到⼀个质数 *x* 之后,可以从该数的 *x* 倍向后筛,因此⼩于 *x* 的倍数⼀定被之前筛过了。
代码实现:
```cpp
bool st[N]; // 当前这个数有没有被筛掉
int p[N]; // 记录质数
int cnt; // 统计质数个数
// 埃⽒筛
void get_prime()
{
for(LL i = 2; i <= n; i++)
{
if(!st[i]) // 没有被标记,说明是质数
{
p[++cnt] = i; // 记录这个质数
// 从 i*i 开始,因为⼩于 i 的倍数已经被划掉了
for(LL j = i * i; j <= n; j += i) // 筛掉这个质数的倍数
{
st[j] = true;
}
}
}
}
```
时间复杂度:
埃⽒筛的时间复杂度为: *O* ( *n* log log *n* ) 。 关于时间复杂度,⽹上各个地⽅都有详细的证明。因为很⿇烦,⽽且涉及积分的知识,这⾥就不再赘 述。
*** ** * ** ***
### 【第⼆种筛法:线性筛法】
线性筛法,⼜称 **欧拉筛法** 。算法思想:
• 在埃⽒筛法中,它会将⼀个合数重复多次标记。如果能让每个合数都只被标记⼀次,那么时间复杂 度就可以降到 *O* ( *n* ) 了。
我们的做法是,让每⼀个合数被它的最⼩质因数筛掉。
代码实现:
```cpp
int n, q;
bool st[N];
int p[N], cnt;
void get_prime()
{
for(int i = 2; i <= n; i++)
{
if(!st[i]) p[++cnt] = i; // 如果没标记过,就是质数
// 枚举所有的质数
for(int j = 1; 1ll * i * p[j] <= n; j++)
{
st[i * p[j]] = true;
if(i % p[j] == 0) break;
/*
这个判定条件能让每⼀个合数被⾃⼰的最⼩质因数筛掉。
1. 如果 i 是合数,枚举到最⼩质因数的时候跳出循环
2. 如果 i 是质数,枚举到⾃⾝时跳出循环
注意,在筛的过程中,我们还能知道 p[j] 是 i 的最⼩质因数
*/
}
}
}
```
*** ** * ** ***
时间复杂度:
每个数只会被⾃⾝最⼩的质因数筛掉⼀次,时间复杂度为 *O* ( *n* ) 。
注意注意注意:
这个算法是⾮常⾮常⾮常重要的!后续很多算法,都是在欧拉筛的基础上实现的。因此,⼀定要理解
这个算法的本质,不能只是简单的背下来。
*** ** * ** ***
### 3.1 【模板】线性筛素数
题⽬来源: 洛⾕
题⽬链接: [P3383 【模板】线性筛素数](http://3.1%20【模板】线性筛素数%20题⽬来源%ef%bc%9a%20洛⾕%20题⽬链接%ef%bc%9a%20P3383%20【模板】线性筛素数%20难度系数%ef%bc%9a%20★★ " P3383 【模板】线性筛素数")
难度系数: ★★
题目背景
本题已更新,从判断素数改为了查询第 k 小的素数。
提示:本题输入输出、运算数据量较大。
* 对于 C++ 语言,如果你使用 `cin` 来输入输出,建议使用 `std::ios::sync_with_stdio(0)` 来加速,同时使用 `'\n'` 换行输出。
* 对于 Java 语言,使用线性筛并且优化输入输出,也可以在规定时限内通过本题,但是时限可能较紧张。
* 对于 Python 语言,语言性能差异较大,需要使用到 `numpy` 库的数组以替代列表,且使用埃氏筛法,依然可以在合适的时间和内存消耗下通过本题。
题目描述
如题,给定一个范围 n,有 q 个询问,每次输出第 k 小的素数。
输入格式
第一行包含两个正整数 n,q,分别表示查询的范围和查询的个数。
接下来 q 行每行一个正整数 k,表示查询第 k 小的素数。
输出格式
输出 q 行,每行一个正整数表示答案。
输入输出样例
**输入 #1**复制
```
100 5
1
2
3
4
5
```
**输出 #1**复制
```
2
3
5
7
11
```
说明/提示
【数据范围】
对于 100% 的数据,n=108,1≤q≤106,保证查询的素数不大于 n。
Data by NaCly_Fish.
### 【解法】
模板题,埃⽒筛和线性筛均可。
*** ** * ** ***
### 【参考代码】
```cpp
#include