[知识点] 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

所以要求最小公倍数的话,求出最大公约数即可。

代码略。

 

 

本文部分参考了:

https://zhuanlan.zhihu.com/p/41759808

posted @ 2020-07-27 21:50  jinkun113  阅读(926)  评论(0编辑  收藏  举报