數論(1):素數

RioTian發表於2020-08-13

素數

我們說,如果存在一個整數 \(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(n\log\log 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) \times (k_2+1) \times (k_3+1) \cdots \times (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\n", 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\n", ans);
  }
  return 0;
}