数论
1. 最大公约数和最小公倍数
【约数和倍数】
- 如果 除以 没有余数,那么 就是 的倍数, 就是 的约数,记作 。 约数,也称因数。
【最大公约数和最小公倍数】
-
最大公约数(Greatest Common Divisor),常缩写为 gcd。 一组整数的公约数,是指同时是这组数中每一个数的约数的数。 一组整数的最大公约数,是指所有公约数里面最大的一个。
-
最小公倍数(Least Common Multiple),常缩写为 lcm。 一组整数的公倍数,是指同时是这组数中每一个数的倍数的数。 一组整数的最小公倍数,是指所有正的公倍数里面,最小的一个数。
求两个数的 gcd 与 lcm 时,有如下性质:
- 对于两个数 和 ,。也就是最大公约数乘以最小公倍数等于两个数的乘积。 因此,一般先求最大公约数,然后用这个性质求最小公倍数。
1.1 欧几里得算法
欧几里得算法也称辗转相除法,可以求出两个整数的最大公约数。
算法流程: 设 :
-
如果 是 的约数,那么 就是两者的最大公约数;
-
如果 不是 的约数,那么 。
因为 会不断减小,因此可以用递归进行求解。
代码实现:
1 LL gcd(LL a, LL b)2 {3 if (!b) return a; // 如果 b 等于 0,说明 a 就是最大公约数4 return gcd(b, a % b);5 }时间复杂度: 求 会遇到两种情况:
-
,则
-
,则
第二种情况会让 至少折半,因此最多执行 次。第一种情况不会多于第二种,因此时间复杂度为 。
1.1.1 例题
B3736 [信息与未来 2018] 最大公约数 输入三个正整数 ,求它们的最大公约数 :最大的正整数 ,满足 都是 的倍数,即 。 输入:12 34 56 输出:2
1 #include <iostream>23 using namespace std;45 int gcd(int a, int b)6 {7 return b == 0 ? a : gcd(b, a % b);8 }910 int main()11 {12 int x, y, z; cin >> x >> y >> z;1314 cout << gcd(gcd(x, y), z) << endl;1516 return 0;17 }1.2 秦九韶算法
秦九韶算法是一种将一元 次多项式的求值问题转化为 个一次式的算法。其大大简化了计算过程,即使在现代,利用计算机解决多项式的求值问题时,秦九韶算法依然是最优的算法。
一个 次多项式: 可以改写成:
例如:对于一个整数 ,可以拆成:
这样对于高精度的数取模,就可以分阶段取模。
1.2.1 例题
牛客>小红的gcb 给两个正整数 ,输出他们的最大公约数 。 表示 的十进制位数,,。
【解法】 先将大数取模,然后再代入公式计算。
1 #include <iostream>23 using namespace std;45 string a;6 int b;78 int gcd(int a, int b)9 {10 return b == 0 ? a : gcd(b, a % b);11 }1213 int calc()14 {15 long long t = 0;16 for (auto ch : a)17 {18 t = t * 10 + ch - '0';19 t %= b;20 }21 return t;22 }2324 int main()25 {26 cin >> a >> b;2728 cout << gcd(b, calc()) << endl;2930 return 0;31 }2. 质数的判定
【质数和合数】
- 一个大于 的自然数,除了 和它自身外,不能被其他自然数整除的数叫做质数;否则称为合数。其中,质数又称素数。 规定 既不是质数也不是合数。
2.1 试除法判断质数
- 对于一个数 ,根据定义,可以从 一个一个尝试,判断 能否被整除。 但是,没有必要每一个都去判断。因为如果 是 的约数,那么 也是 的约数。因此,我们仅需判断较小的 是否是 的约数,没有必要再去考虑 。那么,仅需枚举到 即可。
代码实现:
1 bool isprime(int x)2 {3 if (x <= 1) return false; // 小于等于1的数不考虑45 // 试除法判断是否是质数 - 只需枚举到 sqrt(x)6 for (int i = 2; i <= x / i; i++) // 防溢出的写法7 {8 if (x % i == 0) return false;9 }10 return true;11 }时间复杂度: 枚举到 ,因此时间复杂度为
3. 筛质数
上一个专题学习了如何判断一个数是否是质数,如果此时想知道 中有多少个素数呢?或者是 中的素数里面,第 个素数是多少?
- 一个自然的想法就是从 开始,依次向后对每一个自然数进行一次质数检验。
但是这种解法相对暴力,这里介绍两种方法,能够快速地将 中的素数全部记录下来。
3.1 埃氏筛法
算法思想:
- 对于任意一个大于 的正整数,那么它的 ()倍就是合数。 因此,如果我们从小到大考虑每个数,然后同时把当前这个数的所有倍数记为合数,没有被标记的数就是素数。
小优化:
- 找到一个质数 之后,可以从该数的 倍向后筛,因此小于 的倍数一定被之前筛过了。
时间复杂度: 埃氏筛的时间复杂度为 。
代码实现:
#include <iostream>using namespace std;
typedef long long LL;const int N = 1000010;
bool st[N]; // 当前这个数有没有被筛掉(true表示被筛掉,不是质数)int p[N]; // 记录质数int cnt; // 统计质数个数int n; // 求1~n范围内的质数
void get_prime(){ // 初始化:0和1不是质数 st[0] = st[1] = true;
for (LL i = 2; i <= n; i++) { if (!st[i]) // 没有被标记,说明是质数 { p[++cnt] = i; // 记录这个质数
// 从 i*i 开始,因为小于 i 的倍数已经被之前的质数筛掉了 // 注意:i*i 可能会溢出,所以要用 long long for (LL j = i * i; j <= n; j += i) // 筛掉这个质数的倍数 st[j] = true; } }}
int main(){ cin >> n; get_prime();
// 输出质数 cout << "2~" << n << "之间的质数有:" << endl; for (int i = 1; i <= cnt; i++) cout << p[i] << " "; cout << endl; cout << "共有 " << cnt << " 个" << endl;
return 0;}3.2 第二种筛法:线性筛法
线性筛法,又称欧拉筛法。算法思想:
-
在埃氏筛法中,它会将一个合数重复多次标记。如果能让每个合数都只被标记一次,那么时间复杂度就可以降到 了。
-
我们的做法是,让每一个合数被它的最小质因数筛掉。
时间复杂度: 每个数只会被自身最小的质因数筛掉一次,时间复杂度为 。
代码实现:
1 int n, q;2 bool st[N];3 int p[N], cnt;45 // 欧拉筛6 void get_prime()7 {8 for (int i = 2; i <= n; i++)9 {10 if (!st[i]) p[++cnt] = i; // 如果没标记过,就是质数1112 // 枚举所有的质数13 for (int j = 1; 1ll * i * p[j] <= n; j++)14 {15 st[i * p[j]] = true;16 if (i % p[j] == 0) break;17 // 这个判定条件能让每一个合数被自己的最小质因数筛掉。18 // 1. 如果 i 是合数,枚举到最小质因数的时候跳出循环19 // 2. 如果 i 是质数,枚举到自身时跳出循环20 // 注意,在筛的过程中,我们还能知道 p[j] 是 i 的最小质因数21 }22 }23 }3.3 例题
UVA543 Goldbach’s Conjecture 【题目描述】 哥德巴赫猜想的内容如下: 任意一个大于 的偶数都可以拆成两个奇质数之和。 比如: 你的任务是:验证小于 的数满足哥德巴赫猜想。
【输入描述】 输入包含多组数据。每组数据占一行,包含一个偶数 。读入以 结束。
【输出描述】
对于每组数据,输出形如 ,其中 是奇素数。若有多组满足条件的 ,输出 最大的一组。若无解,输出 Goldbach's conjecture is wrong.。
输入:
820420输出:
8 = 3 + 520 = 3 + 1742 = 5 + 37代码:
#include <iostream>
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;}4. 算术基本定理
【算术基本定理】 算术基本定理又称唯一分解定理:
- 任何一个大于 的自然数 ,都可以唯一分解成有限个质数的乘积 ; 这里 均为质数,其中指数 是正整数。这样的分解称为 的标准分解式。
4.1 分解质因数
- 分解质因数就是将一个合数用质因数相乘的形式表示出来,例如 。
4.1.1 试除法分解质因数
- 的所有因数中,不会有两个大于 。 因此,枚举 中所有的数,如果能整除 就一直除下去。如果最后剩下的数大于 ,那就是大于 的因子。
小优化:
- 如果我们提前打出来一个素数表,枚举所有素数的话,时间复杂度能进一步降低。
时间复杂度: 枚举到 ,因此时间复杂度为 。但是最优情况下会达到 。
代码实现:
1 int c[N]; // c[i] 表示 i 这个质数出现的次数23 void deprime(int x)4 {5 for (int i = 2; i <= x / i; i++)6 {7 int cnt = 0;8 while (x % i == 0) // 只要有这个因子,就除尽,并且计数9 {10 x /= i;11 cnt++;12 }13 c[i] += cnt;14 }15 if (x > 1) c[x]++; // 不要忘记判断最后一个质数16 }4.1.2 例题
题目链接:P2043 质因子分解
【题目描述】 对 进行质因子分解。
【输入描述】 输入数据仅有一行包含一个正整数 ,。
【输出描述】 输出数据包含若干行,每行两个正整数 ,中间用一个空格隔开。表示 包含 个质因子 ,要求按 的值从小到大输出。
输入:
10输出:
2 83 45 27 1解法: 对阶乘中的每一个数,分解质因数。
参考代码:
1 #include <iostream>23 using namespace std;45 const int N = 1e4 + 10;67 int n;8 int c[N];910 // 试除法分解质因数11 void deprime(int x)12 {13 for (int i = 2; i <= x / i; i++)14 {15 int cnt = 0;16 while (x % i == 0)17 {18 cnt++;19 x /= i;20 }21 c[i] += cnt;22 }23 // 注意判断最后一个数24 if (x > 1) c[x]++;25 }2627 int main()28 {29 cin >> n;3031 for (int i = 2; i <= n; i++)32 {33 deprime(i);34 }3536 for (int i = 2; i <= n; i++)37 {38 if (c[i])39 {40 cout << i << " " << c[i] << endl;41 }42 }4344 return 0;45 }5. 约数
5.1 求一个整数的所有约数 - 试除法
对于一个整数 ,若 是 的约数,那么 也是 的约数。也就是说,除了完全平方数,约数都是成对出现的。因此,可以用试除法求一个整数的所有约数。 枚举 之间的整数,判断是否能整除 。
试除法也能求出一个整数的约数个数以及约数总和。
时间复杂度: 枚举到 ,因此时间复杂度为 。 因此,一个整数 的约数个数的上限为 。
代码实现:
1 int d[N], cnt;23 void get_d(int x)4 {5 // 注意从 1 开始循环6 for (int i = 1; i <= x / i; i++)7 {8 if (x % i == 0)9 {10 d[++cnt] = i;11 if (i != x / i) d[++cnt] = x / i;12 }13 }14 }5.1.2 求一个整数的所有约数 - 倍数法
如果用试除法分别求每一个数的约数,时间复杂度过高。 可以反过来想,对于每个数 , 中以 为约数的数就是 的倍数,也就是 。 因此可以用倍数法求出 每个数的约数集合。
时间复杂度: 倍的调和数,约为 。
代码实现:
1 int n;2 vector<int> d[N];34 void get_d()5 {6 for (int i = 1; i <= n; i++) // 枚举所有约数7 for (int j = 1; j <= n / i; j++) // 约数的倍数8 d[i * j].push_back(i);9 }5.2 约数个数定理
由唯一分解定理得:。 若用 表示某一个数约数的个数,那么有: ,也就是所有系数加一之后的乘积。
5.2.1 求单个数的约数个数方法
-
第一种方式:枚举 之间的整数;
-
第二种方式:在分解质因数的过程中,利用公式可以直接计算出某个数的约数个数。
5.3约数和定理
由唯一分解定理得:。 若用 表示某一个数约数和,那么有: 。
5.3.1 求单个数的约数之和方法
-
第一种方式:枚举 之间的整数;
-
第二种方式:在分解质因数的过程中,利用公式可以直接计算出某个数的约数总和。
5.3.2 例题
求自然数 的所有约数之和。
输入描述: 输入一行,包含一个正整数 ,范围在 以内。
输出描述: 输出一行,包含一个整数。
输入:10 输出:18
1 #include <iostream>23 using namespace std;45 int main()6 {7 int n; cin >> n;8 int sum = 0;910 for (int i = 1; i <= n / i; i++)11 {12 if (n % i == 0)13 {14 sum += i;15 if (i != n / i) sum += n / i;16 }17 }1819 cout << sum << endl;2021 return 0;22 }5.3.3 例题
【题目描述】 给个 ,求 到 的所有数的约数个数的和。
【输入描述】 第一行一个正整数 。
【输出描述】 输出一个整数,表示答案。
输入:3 输出:5
【解法】 正难则反。 求每个数的约数比较麻烦,对于一个数 ,在 中,一共有 个数,里面的约数有 。因此可以反着求。
1 #include <iostream>23 using namespace std;45 int main()6 {7 int n; cin >> n;89 long long sum = 0;10 for (int i = 1; i <= n / 2; i++) sum += n / i;11 sum += n - n / 2;1213 cout << sum << endl;1415 return 0;16 }文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!

