算法基础(数学)—数论

【算法竞赛中的数学】

1. 《数论》

数论是纯粹数学的分⽀,主要研究的是整数的性质,被称为"最纯"的数学领域。
数论中有很多描述巨简单,但是证明很困难的理论。因此,在讲解数论相关的内容时,采取的⽅式是 先使⽤,后证明。

2. 《组合数学》

组合数学,在总体上是⼀⻔研究可数或离散对象的科学。它可分为⼴义上的和狭义上的两种层⾯,若 是前者 (⼴义的组合数学) ,其相当于离散数学,⽽后者 (狭义的组合数学) 则是组合计数、图论、代数 结构、数理逻辑等的总称,但这只是不同学者在称谓上的区别。
竞赛中的组合数学的主要内容就是组合计数,容斥原理。

3. 《线性代数》

线性代数是关于向量空间和线性映射的⼀个数学分⽀。主要研究向量,向量空间,矩阵理论,线性变 换以及有线维线性⽅程组等等。
竞赛中常⽤到的就是矩阵乘法以及⾼斯消元。

4. 《博弈论》

博弈论,⼜译为对策论,研究游戏或者博弈内的相互作⽤,是研究具有⽃争或竞争性质现象的数学理 论和⽅法。也是运筹学的⼀个重要学科。

5. 《概率论》

概率论是研究概率、随机性及不确定性等现象的数学分⽀。 竞赛中有关概率论的题⽬⼤多出现在动态规划中,会在《算法提⾼篇》重点讲解。

6. 《计算⼏何》

计算⼏何是⼏何外形信息的计算机表⽰、分析和综合。

【⼩要求】

• 学习数学千万不要只是去听,⼀定要把纸和笔拿出来,把课上的例⼦以及算法流程复刻⼀遍。


1. 最⼤公约数和最⼩公倍数

【约数和倍数】

• 如果 a除以 b没有余数,那么 a就是 b的倍数, b就是 a的约数,记作 ba。 约数,也称因数。

【最⼤公约数和最⼩公倍数】

最⼤公约数 Greatest Common Divisor,常缩写为 gcd。
• ⼀组整数的公约数,是指同时是这组数中每⼀个数的约数的数。
• ⼀组整数的最⼤公约数,是指所有公约数⾥⾯最⼤的⼀个。

最⼩公倍数 Least Common Multiple,常缩写为 lcm。

• ⼀组整数的公倍数,是指同时是这组数中每⼀个数的倍数的数。
• ⼀组整数的最⼩公倍数,是指所有正的公倍数⾥⾯,最⼩的⼀个数。

求两个数的 gcd 与 lcm 时,有如下性质:
• 对于两个数 abgcd ( a , b ) × lcm ( a , b ) = a × b 。也就是 最⼤公约数乘以最⼩公倍数等于两
个数的乘积。
因此,⼀般先求最⼤公约数,然后⽤这个性质求最⼩公倍数。

【欧⼏⾥得算法】

欧⼏⾥得算法也称辗转相除法,可以求出两个整数的最⼤公约数。
算法流程:
a> b
• 如果 ba的约数,那么 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 ) 会遇到两种情况:

  1. a < b ,则 gcd ( a , b ) = gcd ( b , a )
  2. 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 using namespace std; typedef long long LL; const int N = 1e8 + 10; int n, q; bool st[N]; int p[N], cnt; // 埃⽒筛 // void get_prime() // { // for(LL i = 2; i <= n; i++) // { // if(!st[i]) // 质数 // { // p[++cnt] = i; // for(LL j = i * i; j <= n; j += i) // { // st[j] = true; // } // } // } // } // 线性筛 void get_prime() { for(LL i = 2; i <= n; i++) { if(!st[i]) p[++cnt] = i; for(int j = 1; i * p[j] <= n; j++) { st[i * p[j]] = true; if(i % p[j] == 0) break; } } } int main() { scanf("%d%d", &n, &q); get_prime(); while(q--) { int k; scanf("%d", &k); printf("%d\n", p[k]); } return 0; } ``` *** ** * ** *** ### 3.2 素数密度 题⽬来源: 洛⾕ 题⽬链接: [P1835 素数密度](https://www.luogu.com.cn/problem/P1835 " P1835 素数密度") 难度系数: ★★★ 题目背景 UPD: * 2024.8.12:加入一组 Hack 数据。 题目描述 给定 L,R,请计算区间 \[L,R\] 中素数的个数。 1≤L≤R\<231,R−L≤106。 输入格式 第一行,两个正整数 L 和 R。 输出格式 一行,一个整数,表示区间中素数的个数。 输入输出样例 **输入 #1**复制 ``` 2 11 ``` **输出 #1**复制 ``` 5 ``` *** ** * ** *** ### 【解法】 • 埃⽒筛 + 线性筛。 先筛出 \[1, *r* \] 之间的质数,然后⽤这些质数去筛 \[ *l* , *r* \] 区间的质数。 *** ** * ** *** ### 【参考代码】 ```cpp #include #include using namespace std; typedef long long LL; const int N = 1e6 + 10; int l, r; bool st[N]; int p[N], cnt; bool ret[N]; void get_prime() { int n = sqrt(r); 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; } } } int main() { cin >> l >> r; get_prime(); l = l == 1 ? 2 : l; for(int i = 1; i <= cnt; i++) { LL x = p[i]; for(LL j = max(x * 2, (x + l - 1) / x * x); j <= r; j += x) { ret[j - l] = true; } } int sum = 0; for(int i = l; i <= r; i++) { if(!ret[i - l]) sum++; } cout << sum << endl; return 0; } ``` *** ** * ** *** ### 3.3 Goldbach's Conjecture 题⽬来源: 洛⾕ 题⽬链接: [UVA543 Goldbach's Conjecture](https://www.luogu.com.cn/problem/UVA543 "UVA543 Goldbach's Conjecture") 难度系数: ★★ *** ** * ** *** 题目描述 哥德巴赫猜想的内容如下: 任意一个大于 4 的偶数都可以拆成两个奇质数之和。 比如: 82042​=3+5=3+17=7+13=5+37=11+31=13+29=19+23​ 你的任务是:验证小于 106 的数满足哥德巴赫猜想。 输入格式 输入包含多组数据。 每组数据占一行,包含一个偶数 n(n≤106)。 读入以 0 结束。 输出格式 对于每组数据,输出形如 `n = a + b`,其中 a,b 是奇质数。若有多组满足条件的 a,b,输出 b−a 最大的一组。 若无解,输出 `Goldbach's conjecture is wrong.`。 显示翻译 题意翻译 输入输出样例 **输入 #1**复制 ``` 8 20 42 0 ``` **输出 #1**复制 ``` 8 = 3 + 5 20 = 3 + 17 42 = 5 + 37 ``` *** ** * ** *** ### 【解法】 先筛⼀下质数,然后在筛出来的质数中,找出两个数的和等于给定的数 *** ** * ** *** ### 【参考代码】 ```cpp #include using namespace std; const int N = 1e6 + 10; bool st[N]; int p[N], cnt; void get_prime() { int n = 1e6; 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; } } } void solve(int x) { for(int i = 2; i <= cnt; i++) { if(!st[x - p[i]]) { printf("%d = %d + %d\n", x, p[i], x - p[i]); break; } } } int main() { get_prime(); int x; while(cin >> x, x) { solve(x); } return 0; } ```

相关推荐
暴风游侠16 小时前
如何进行科学的分类
笔记·算法·分类
羊小猪~~17 小时前
【QT】-- QT基础类
开发语言·c++·后端·stm32·单片机·qt
leaves falling17 小时前
冒泡排序(基础版+通用版)
数据结构·算法·排序算法
C雨后彩虹17 小时前
无向图染色
java·数据结构·算法·华为·面试
坚持就完事了17 小时前
扫描线算法
算法
努力写代码的熊大17 小时前
深入探索C++关联容器:Set、Map、Multiset与Multimap的终极指南及底层实现剖析
开发语言·c++
鱼跃鹰飞17 小时前
Leetcode尊享面试100题:252. 会议室
算法·leetcode·面试
程序员-King.17 小时前
二分查找——算法总结与教学指南
数据结构·算法
Zevalin爱灰灰17 小时前
现代控制理论——第三章 线性控制系统的能控性和能观性
线性代数·算法·现代控制
kklovecode17 小时前
C语言之头文件,宏和条件编译
c语言·开发语言·算法