[知识点] 6.4.1 素数与最大公约数
总目录 > 6 数学 > 6.4 数论 > 6.4.1 素数与最大公约数
前言
数论开始。这一块知识点还挺凌乱的,又多又杂。
子目录列表
1、素数
2、素数判定
3、反素数
4、最大公约数与最小公倍数
6.4.1 素数与最大公约数
1、素数
对于正整数 a, d,如果存在正整数 k,使得 a = kd,则称 d 整除 a,记作 d | a。这时,a 是 d 的倍数,而 d 是 a 的约数(因数,因子)。
显然,对于任意大于 1 的整数 a,它都能被 1 和 a 整除,而如果它能且仅能被这两个数整除,则称 a 为素数(质数);否则,则称 a 为合数。举例子:
5 的约数为 1 和 5,则 5 为素数;
12 的约数为 1, 2, 3, 4, 6, 12,则 12 为合数。
1 既不是素数也不是合数。
顺带一提素数计数函数:小于等于 x 的素数的个数,用 π(x) 表示。当 x 趋近于无穷大时,π(x) 趋近于 x / lnx。
2、素数判定
判定 a 是否为素数,最暴力的做法是从 2 到 a - 1 逐一判断看是否能整除,时间复杂度为 O(n),但其实是没必要的,很容易发现,如果 d 是 a 的约数,则 a / d 也是 a 的约数,就如上述 12 的 6 个约数,可以组成 1 * 12, 2 * 6, 3 * 4 三组,而我们只需要对这些组较小的那个数进行判断即可,显然,这些较小数是恒小于等于 a 的平方根的。这样,我们的时间复杂度骤降至 O(sqrt(n))。代码如下:
bool isPrime(int o) { for (int i = 2; i * i <= o; i++) if (o % i == 0) return 0; return 1; }
同样地,这个判定算法还可以用于求任意数的因子个数。代码如下:
int cnt(int o) { if (o == 1) return 1; int ans = 0; for (int i = 2; i * i <= o; i++) { ans += (o % i == 0) * 2; if (i * i == o) ans--; } return ans + 2; }
还有许多高大上的素数判定方法,诸如 Miller-Rabin 素性测试,Fermat 素性测试,卡迈克尔数等等概念,此处暂略。
3、素数筛法
① Eratosthenes 筛法
知道了素数的判定方法,那么如何快速筛选出素数?小于等于 n 的素数有多少?最直接的是对 [1, n] 逐一判断,时间复杂度为 O(n * sqrt(n)),重复劳动太多了!
易知对于整数 x,其倍数 2x, 3x, ..., kx 必然是合数,那么我们可以对每次当前考虑的数的所有倍数进行一个标记,表示非素数,那么在以后判断时可以直接跳过。并且,可以确定的是,只要当前数没有在之前被其他数的倍数标记为合数,那么它必定是素数,也就不再需要再次判断。
假设筛选 12 以内的素数。开始判断 2 是否为素数,然后对 4, 6, 8, 10, 12 标记为合数;再判断 3 是否为素数,对 6, 9 标记为合数;4 是合数;5 是素数,对 10 标记为合数……7, 11 是素数,6, 8, 10, 12 是合数。综上,总共只判断了 5 次。
代码:
1 int cnt(int o) { 2 int ans = 0; 3 for (int i = 2; i <= o; i++) p[i] = 1; 4 for (int i = 2; i <= o; i++) { 5 if (!p[i]) continue; 6 ans++; 7 for (int j = i * 2; j <= o; j += i) 8 p[j] = 0; 9 } 10 return ans; 11 }
p[i] = 1 时表示 i 为素数,初始默认从 2 开始的所有数均为素数。
从一定范围内筛选出满足特定要求的数的算法,称为筛法。上述筛素数的方法为 Eratosthenes 筛法(埃拉托斯特尼筛法),其时间复杂度为 O(n log log n)。
②
在 Eratosthenes 筛法中我们注意到,对合数的标记过程仍存在重复 —— 比如判断 2 时,我们会标记 2 * 3 = 6;判断 3 时,我们又会标记 3 * 2 = 6。一旦 n 值够大,无意义的步骤也会随之增多,如何优化呢?
3、反素数
① 概念
对于正整数 n,如果任何小于它的正整数的因子个数都小于它的因子个数,则称 n 为反素数,比如 1, 2, 4, 6, 12, ...
为什么这样定义呢?因为素数可以认为是因子最少的数,反素数则是对于一个特定集合的因子最多的数,而这个特定集合就是 1 到这个数本身。
② 求解
如何求解反素数?要求一个数的因子个数,首先需要分解质因子,即把 n 分解成 n = (p1 ^ k1) * (p2 ^ k2) * ... * (pn ^ kn) 的形式,其中 p 为素数,k 为对应 p 的指数,这样 n 的因子个数为 (k1 + 1) * (k2 + 1) * ... * (kn + 1)。并且,容易得到以下两个推论:
> 假设 p1 < p2 < ... < pn,那么 p1, p2, ..., pn 肯定是从 2 开始的连续素数数列;
> 假设 p1 < p2 < ... < pn,那么肯定满足 k1 >= k2 >= ... >= kn。
两个推论均可通过反证法证明。
根据这两个推论进行枚举就行啦。
代码(给定因子数求其最小数):
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 #define INF 1 << 30 5 6 const int p[16] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53}; 7 8 int ans = INF, n; 9 10 void dfs(int o, int x, int d, int k) { 11 if (o > n || d >= 16) return; 12 if (o == n && ans > x) { 13 ans = x; 14 return; 15 } 16 for (int i = 1; i <= k; i++) { 17 if (x / p[d] > ans) break; 18 dfs(o * (i + 1), x = x * p[d], d + 1, i); 19 } 20 } 21 int main() { 22 cin >> n; 23 dfs(1, 1, 0, 32); 24 cout << ans; 25 return 0; 26 }
对于 DFS,o 表示当前因子数,x 表示当前因子得到的值,d 表示当前枚举的是第 d 个素数,k 表示当前枚举数的最高指数。
如果问题变成给定数的范围,求范围内因子数最多的数,那么返回条件和结果记录稍作调整即可,如下代码:
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 #define INF 1 << 30 5 6 const int p[16] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53}; 7 8 int ans = INF, n, mx; 9 10 void dfs(int o, int x, int d, int k) { 11 if (x > n || d >= 16) return; 12 if (o > mx) mx = o, ans = x; 13 for (int i = 1; i <= k; i++) { 14 if (x * p[d] > n) break; 15 dfs(o * (i + 1), x = x * p[d], d + 1, i); 16 } 17 } 18 int main() { 19 cin >> n; 20 dfs(1, 1, 0, 32); 21 cout << ans; 22 return 0; 23 }
4、最大公约数与最小公倍数
① 最大公约数
一组数的公约数,是指这组数中每个数共有的约数;而最大公约数(Greatest Common Divisor,通常缩写成 ***),指的是公约数中的最大数。举例子:
12, 18 的公约数有 1, 2, 3, 6,最大公约数为 12;
23, 24 的公约数有 1,最大公约数为 1。
那么如何求最大公约数呢?那就是大名鼎鼎的欧几里得算法与其高级版本扩展欧几里得算法了。因为之前单独开篇讲过一次,所以直接链接过去:6.4.2 欧几里得算法与扩欧算法
证明暂略。
② 最小公倍数
一个数为一组数的公倍数,当且仅当这组数的每个数都是它的约数;而最小公倍数(Least Common Multiple,通常缩写成 LCM),指的是公倍数中的最小数。举例子:
5, 16 的公倍数有 80, 160, ...,最小公倍数为 80;
6, 9 的公倍数有 18, 36, ...,最小公倍数为 18。
那么如何求最小公倍数呢?我们联系到它的孪生兄弟最大公约数。
求解反素数的时候我们就稍微提到了一下,每个正整数都可以表示为若干带指数的素数乘积,且是唯一的。
设 a = (p1 ^ ka1) * (p2 ^ ka2) * ... * (pn ^ kan), b = (p1 ^ kb1) * (p2 ^ kb2) * ... * (pn ^ kbn) ,k 可以为 0,则可以得到:
两者的最大公约数为 gcd(a, b) = (p1 ^ min(ka1, kb1)) * (p2 ^ min(ka1, kb1)) * ... * (pn ^ min(ka1, kb1));
两者的最小公倍数为 lcm(a, b) = (p1 ^ max(ka1, kb1)) * (p2 ^ max(ka1, kb1)) * ... * (pn ^ max(ka1, kb1));
又易得,ka + kb = max(ka, kb) + min(ka, kb),则可得到如下结论:
gcd(a, b) * lcm(a, b) = a * b
所以要求最小公倍数的话,求出最大公约数即可。
代码略。
本文部分参考了: