zoukankan      html  css  js  c++  java
  • [知识点] 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

  • 相关阅读:
    JavaScript 垃圾回收
    JavaScript 跳坑指南
    javaScript AJAX
    高效 JavaScript
    Java使用 Thumbnails 压缩图片
    Vue前端压缩图片
    JS input输入框字数超出长度显示省略号.....
    Vue图片浏览组件vviewer使用
    浏览器获取京东cookie
    图片在容器内水平垂直居中显示
  • 原文地址:https://www.cnblogs.com/jinkun113/p/13387901.html
Copyright © 2011-2022 走看看