• 数论(1):素数


    素数

    我们说,如果存在一个整数 (k) ,使得 (a = kd) ,则称 (d) 整除 (a) ,记做 (d mid a) ,称 (a)(d) 的倍数,如果 (d > 0) ,称 (d)(a) 的约数。特别地,任何整数都整除 (0)

    显然大于 (1) 的正整数 (a) 可以被 (1)(a) 整除,如果除此之外 (a) 没有其他的约数,则称 (a) 是素数,又称质数。任何一个大于 (1) 的整数如果不是素数,也就是有其他约数,就称为是合数。 (1) 既不是合数也不是素数。

    素数计数函数:小于或等于 (x) 的素数的个数,用 (pi(x)) 表示。随着 (x) 的增大,有这样的近似结果: (pi(x) sim frac{x}{ln(x)})

    素数判定

    我们自然地会想到,如何用计算机来判断一个数是不是素数呢?

    暴力做法

    自然可以枚举从小到大的每个数看是否能整除

    bool isPrime(a) {
      if (a < 2) return 0;
      for (int i = 2; i < a; ++i)
        if (a % i == 0) return 0;
      return 1;
    }
    

    这样做是十分稳妥了,但是真的有必要每个数都去判断吗?

    很容易发现这样一个事实:如果 (x)(a) 的约数,那么 (frac{a}{x}) 也是 (a) 的约数。

    这个结论告诉我们,对于每一对 ((x, frac{a}{x} )) ,只需要检验其中的一个就好了。为了方便起见,我们之考察每一对里面小的那个数。不难发现,所有这些较小数就是 ([1, sqrt{a}]) 这个区间里的数。

    由于 (1) 肯定是约数,所以不检验它。

    bool isPrime(a) {
      if (a < 2) return 0;
      for (int i = 2; i * i <= a; ++i)
        if (a % i) return 0;
      return 1;
    }
    

    Eratosthenes筛法

    现在已经知道了单个数字如何去判断是否是素数,但对于区间判定呢?

    熟悉我的博客的人应该知道以前我发布过 埃拉托斯特尼筛法 的详解blog。

    但还是这里贴一下算法实现:

    int Eratosthenes(int n) {
      int p = 0;
      for (int i = 0; i <= n; ++i) is_prime[i] = 1;
      is_prime[0] = is_prime[1] = 0;
      for (int i = 2; i <= n; ++i) {
        if (is_prime[i]) {
          prime[p++] = i;  // prime[p]是i,后置自增运算代表当前素数数量
          for (int j = i * i; j <= n;
               j += i)  // 因为从 2 到 i - 1 的倍数我们之前筛过了,这里直接从 i
                        // 的倍数开始,提高了运行速度
            is_prime[j] = 0;  //是i的倍数的均不是素数
        }
      }
      return p;
    }
    

    以上为 Eratosthenes 筛法 (埃拉托斯特尼筛法),时间复杂度是 (O(nloglog n))

    线性筛法

    以上做法仍有优化空间,我们发现这里面似乎会对某些数标记了很多次其为合数。有没有什么办法省掉无意义的步骤呢?

    答案当然是:有!

    如果能让每个合数都只被标记一次,那么时间复杂度就可以降到 (O(n))

    经典的Eratosthenes筛法,它可能对同一个质数筛去多次。那么如果用某种方法使得每个合数只被筛去一次就变成是线性的了。
    不妨规定每个合数只用其最小的一个质因数去筛,这便是线性筛法了。

    void euler_sieve(int n)
    {
        totPrimes = 0;//质数数量
        memset(flag, 0, sizeof(flag));//最小质因子
    
        for (int i = 2; i <= n; i++) {
            if (!flag[i])
                primes[totPrimes++] = i;//i是质数
            //给当前的数i乘上一个质因子
            for (int j = 0; i * primes[j] <= n; j++) {
                flag[i*primes[j]] = true;
                //请仔细体会i % primes[j] == 0的含义。
                //利用了每个合数必有一个最小素因子,每个合数仅被它的最小素因子筛去正好一次,所以是线性时间。
                if (i % primes[j] == 0)
                    break;
            }
        }
    }
    

    上面的这种 线性筛法 也称为 Euler 筛法 (欧拉筛法)。

    Euler筛法的一个小证明:here

    题外话:欧拉筛法可以求欧拉函数,这里就不展开了。


    说了这么久筛法,那到底筛法有什么用呢?

    对于求约数个数、约数和能起很大作用!

    筛法求约数个数

    参考文章:Here

    (d_i) 表示 (i) 的约数个数, (num_i) 表示 (i) 的最小质因子出现次数。

    约数个数定理

    定理:若 (n=prod_{i=1}^mp_i^{c_i})(d_i=prod_{i=1}^mc_i+1) .

    证明:我们知道 (p_i^{c_i}) 的约数有 (p_i^0,p_i^1,dots ,p_i^{c_i})(c_i+1) 个,根据乘法原理, (n) 的约数个数就是 (prod_{i=1}^mc_i+1) .

    实现

    因为 (d_i) 是积性函数,所以可以使用线性筛。

    void pre() {
      d[1] = 1;
      for (int i = 2; i <= n; ++i) {
        if (!v[i]) v[i] = 1, p[++tot] = i, d[i] = 2, num[i] = 1;
        for (int j = 1; j <= tot && i <= n / p[j]; ++j) {
          v[p[j] * i] = 1;
          if (i % p[j] == 0) {
            num[i * p[j]] = num[i] + 1;
            d[i * p[j]] = d[i] / num[i * p[j]] * (num[i * p[j]] + 1);
            break;
          } else {
            num[i * p[j]] = 1;
            d[i * p[j]] = d[i] * 2;
          }
        }
      }
    }
    

    筛法求约数和

    参考文章:Here

    (f_i) 表示 (i) 的约数和, (g_i) 表示 (i) 的最小质因子的 (p+p^1+p^2+dots p^k) .

    void pre() {
      g[1] = f[1] = 1;
      for (int i = 2; i <= n; ++i) {
        if (!v[i]) v[i] = 1, p[++tot] = i, g[i] = i + 1, f[i] = i + 1;
        for (int j = 1; j <= tot && i <= n / p[j]; ++j) {
          v[p[j] * i] = 1;
          if (i % p[j] == 0) {
            g[i * p[j]] = g[i] * p[j] + 1;
            f[i * p[j]] = f[i] / g[i] * g[i * p[j]];
            break;
          } else {
            f[i * p[j]] = f[i] * f[p[j]];
            g[i * p[j]] = 1 + p[j];
          }
        }
      }
      for (int i = 1; i <= n; ++i) f[i] = (f[i - 1] + f[i]) % Mod;
    }
    

    反素数

    定义

    如果某个正整数 (n) 满足如下条件,则称为是反素数:
    任何小于 (n) 的正数的约数个数都小于 (n) 的约数个数

    注:注意区分 emirp ,它是用来表示从后向前写读是素数的数。

    简介

    (本段转载自 桃酱的算法笔记 ,原文戳 链接 ,已获得作者授权)

    其实顾名思义,素数就是因子只有两个的数,那么反素数,就是因子最多的数(并且因子个数相同的时候值最小),所以反素数是相对于一个集合来说的。

    我所理解的反素数定义就是,在一个集合中,因素最多并且值最小的数,就是反素数。

    那么,如何来求解反素数呢?

    首先,既然要求因子数,我首先想到的就是素因子分解。把 (n) 分解成 (n=p_{1}^{k_{1}}p_{2}^{k_{2}} cdots p_{n}^{k_{n}}) 的形式,其中 (p) 是素数, (k) 为他的指数。这样的话总因子个数就是 ((k_1+1) imes (k_2+1) imes (k_3+1) cdots imes (k_n+1))

    但是显然质因子分解的复杂度是很高的,并且前一个数的结果不能被后面利用。所以要换个方法。

    我们来观察一下反素数的特点。

    1. 反素数肯定是从 (2) 开始的连续素数的幂次形式的乘积。

    2. 数值小的素数的幂次大于等于数值大的素数,即 (n=p_{1}^{k_{1}}p_{2}^{k_{2}} cdots p_{n}^{k_{n}}) 中,有 (k_1 geq k_2 geq k_3 geq cdots geq k_n)

    解释:

    1. 如果不是从 (2) 开始的连续素数,那么如果幂次不变,把素数变成数值更小的素数,那么此时因子个数不变,但是 (n) 的数值变小了。交换到从 (2) 开始的连续素数的时候 (n) 值最小。

    2. 如果数值小的素数的幂次小于数值大的素数的幂,那么如果把这两个素数交换位置(幂次不变),那么所得的 (n) 因子数量不变,但是 (n) 的值变小。

    另外还有两个问题,

    1. 对于给定的 (n) ,要枚举到哪一个素数呢?

      最极端的情况大不了就是 (n=p_{1}p_{2} cdots p_{n}) ,所以只要连续素数连乘到刚好小于等于 (n) 就可以的呢。再大了,连全都一次幂,都用不了,当然就是用不到的啦!

    2. 我们要枚举到多少次幂呢?

      我们考虑一个极端情况,当我们最小的素数的某个幂次已经比所给的 (n) (的最大值)大的话,那么展开成其他的形式,最大幂次一定小于这个幂次。unsigned long long 的最大值是 2 的 64 次方,所以我这边习惯展开成 2 的 64 次方。

    细节有了,那么我们具体如何具体实现呢?

    我们可以把当前走到每一个素数前面的时候列举成一棵树的根节点,然后一层层的去找。找到什么时候停止呢?

    1. 当前走到的数字已经大于我们想要的数字了

    2. 当前枚举的因子已经用不到了(和 (1) 重复了嘻嘻嘻)

    3. 当前因子大于我们想要的因子了

    4. 当前因子正好是我们想要的因子(此时判断是否需要更新最小 (ans)

    然后 dfs 里面不断一层一层枚举次数继续往下迭代就好啦~~

    常见题型

    求因子数一定的最小数

    题目链接: https://codeforces.com/problemset/problem/27/E

    对于这种题,我们只要以因子数为 dfs 的返回条件基准,不断更新找到的最小值就可以了

    上代码:

    #include <stdio.h>
    #define ULL unsigned long long
    #define INF ~0ULL
    ULL p[16] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53};
    
    ULL ans;
    ULL n;
    
    // depth: 当前在枚举第几个素数。num: 当前因子数。
    // temp: 当前因子数量为 num
    // 的时候的数值。up:上一个素数的幂,这次应该小于等于这个幂次嘛
    void dfs(ULL depth, ULL temp, ULL num, ULL up) {
      if (num > n || depth >= 16) return;
      if (num == n && ans > temp) {
        ans = temp;
        return;
      }
      for (int i = 1; i <= up; i++) {
        if (temp / p[depth] > ans) break;
        dfs(depth + 1, temp = temp * p[depth], num * (i + 1), i);
      }
    }
    
    int main() {
      while (scanf("%llu", &n) != EOF) {
        ans = INF;
        dfs(0, 1, 1, 64);
        printf("%llu
    ", ans);
      }
      return 0;
    }
    

    求 n 以内因子数最多的数

    http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=1562

    思路同上,只不过要改改 dfs 的返回条件。注意这样的题目的数据范围,我一开始用了 int,应该是溢出了,在循环里可能就出不来了就超时了。上代码,0ms 过。注释就没必要写了上面写的很清楚了。

    #include <iostream>
    #define ULL unsigned long long
    
    int p[16] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53};
    ULL n;
    ULL ans, ans_num;  // ans 为 n 以内的最大反素数(会持续更新),ans_sum 为 ans
                       // 的因子数。
    
    void dfs(int depth, ULL temp, ULL num, int up) {
      if (depth >= 16 || temp > n) return;
      if (num > ans_num) {
        ans = temp;
        ans_num = num;
      }
      if (num == ans_num && ans > temp) ans = temp;
      for (int i = 1; i <= up; i++) {
        if (temp * p[depth] > n) break;
        dfs(depth + 1, temp *= p[depth], num * (i + 1), i);
      }
      return;
    }
    
    int main() {
      while (scanf("%llu", &n) != EOF) {
        ans_num = 0;
        dfs(0, 1, 1, 60);
        printf("%llu
    ", ans);
      }
      return 0;
    }
    
  • 相关阅读:
    在Ubuntu1804上使用Apache2的部署Django配置
    UbuntuServer1804设置uwsgi自启动服务
    ubuntu 安装k8s 1.22.3 (VirtualBox虚拟机)
    启动keepalived 报错
    wasm-pack 编译错误 unexpected character 'u{0}'
    mariadb-安装
    K8S1.18 安装教程
    Ubuntu共享文件权限问题
    docker 安装consul
    Ubuntu 安装 MySQL 和远程连接
  • 原文地址:https://www.cnblogs.com/RioTian/p/13497646.html
Copyright © 2020-2023  润新知