素數

revkiru發表於2024-11-29

可能出現“質數”、“素數”混用的情況,見諒。

定義

一個正整數無法被除了 \(1\) 和它自身之外的任何自然數整除,則稱該數為質數,否則稱其為合數。

注意到在整個自然數集合中,質數數量不多、分佈稀疏,對於一個 足夠大\(N \in \mathbb{Z}\)\(\leq N\) 的質數大約有 \(\frac{N}{\ln N}\) 個。換句話說,\(\ln N\) 個數中大約有 \(1\) 個質數。

質數判定與篩選

試除法

我們知道若 \(N \in \mathbb{Z}\)\(N\) 為合數,則必定存在一個能整除 \(N\) 的數 \(M\) 滿足 \(2 \leq M \leq \sqrt{N}\)

可以寫出程式碼:

bool IsPrime(int x) {
    if (x == 0 || x == 1)
        return 0;
    for (int i = 2; i * i <= x; i++) {
        if (x % i == 0)
            return 0;
    }
    return 1;
}

基於隨機化的判定有 Miller-Robbin

Eratosthenes

思想是對於任意 \(x\) 的倍數 \(x, 2x, 3x, \dots\) 都不是質數,所以從 \(2\) 開始倍數篩掉,往後若一個數 \(x\) 尚未被標記,意味著其不能被 \(\forall [2, x - 1]\) 整除,則其為質數。

然後同上試除法,為了防止形如 \(6 = 3 \times 2\) 同時被 \(2, 3\) 篩兩次的情況(小於 \(x^2\) 的數 \(x\) 的倍數在掃描更小的數就已經篩過了),從 \(x^2\) 開始,標記 \(x^2, (x + 1)x, \dots, \lfloor \frac{N}{x} \rfloor x\) 即可。

void Prime(const int n) {
    memset(v, 0, sizeof(v));
    for (int i = 2; i <= n; i++) {
        if (v[i])
            continue;
        p[++tot] = i;
        for (int j = i; j <= n / i; j++)
            v[i * j] = 1;
    }
}

複雜度為 \(O(\sum_{p \leq N, p \in \mathbb{P}} \frac{N}{p}) = O(N \log \log N)\),接近線性,是 OI 中最常見的篩法。

線性篩

我們是怎麼最佳化暴力篩的?透過列舉倍數來減少總的列舉次數。但是最佳化後我們的埃氏篩還是會重複標記某些合數,例如 \(12\) 會被 \(2 \times 6\)\(3 \times 4\) 同時標記。那我們能否唯一確定出一種篩選出某個數的做法:即我們 能否確定某個數唯一產生的方式

我們正式引入線性篩:線性篩透過“從小到大累積質因子”的方式標記每個合數,這裡直接給出易懂的程式碼:

處理的手段其實就是透過確定唯一產生數的方式來確定唯一的“遍歷”方式。

int v[N], p[N];

void Prime(const int n) {
    m = 0;
    memset(v, 0, sizeof(v));
    for (int i = 2; i <= n; i++) {
        if (!v[i]) { v[i] = i; p[++m] = i; }    // 質數
        // 給當前的數 i 乘上一個質因子
        for (int j = 1; j <= m; j++) {
            // i 有比 p[j] 更小的質因子,或者已經超出了 n 的範圍
            if (p[j] > v[i] || p[j] > n / i)
                break;
            // p[j] 是合數 i * p[j] 的最小質因子
            v[i * p[j]] = p[j];
        }
    }
}

複雜度為 \(O(n)\),真正的線性篩法。

算數基本定理

這非常重要。

任何一個大於 \(1\) 的正整數都能唯一分解有限個質數的乘積,形式化地:

\[N = p_1^{c_1} p_2^{c_2} \cdots p_m^{c_m} \]

其中 \(c_i \in \mathbb{Z}, p_i \in \mathbb{P}, p_1 \lt p_2 \lt \cdots \lt p_m\)

結合試除法和埃氏篩我們就可以進行一個質因數的分解,複雜度為 \(\sqrt{N}\)

void DivideNum(const int n) {
    m = 0;
    for (int i = 2; i * i <= n; i++) {
        if (n % i == 0) {   // i 是質數
            p[++m] = i;
            c[m] = 0;
            while (n % i == 0) { c /= i; c[m]++; }      // 抹掉所有 i
        }
    }
    if (n > 1) { p[++m] = n; c[m] = 1; }    // n 是質數
}

有一種效率更高的 Pollard's Rho 用於最佳化複雜度。

相關文章