初等數論學習筆記 II:分解質因數


初等數論學習筆記 I:同余相關

CHANGE LOG

  • 2022.7.13:重構文章,更新 PR 模板代碼。
  • 2023.1.23:對文章進行修補。

1. Miller-Rabin

Miller-Rabin 素性測試是一種具有隨機性的素數判定方法。它有一定概率將合數判定為素數,但不會將素數判定為合數。

素數判定的基本思路為根據所有質數但很少合數具有的性質,檢查被判定的數是否具有這些性質。若不具有,則該數是合數,否則該數大概率是質數。

1.1 費馬素性檢驗

\(p\) 是素數時,對於任意 \(a \perp p\) 均有 \(a ^ {p - 1}\equiv 1 \pmod p\)。相反,當 \(a ^ {p - 1} \equiv 1\pmod p\) 時,是否有 \(p\) 是素數?

可惜命題並不成立。有極小概率使得 \(a \perp p\)\(p\) 是合數且 \(a ^ {p - 1} \equiv 1\pmod p\),如當 \(a = 2\)\(p = 341\) 時,\(2 ^ {340} \equiv 1 \pmod {341}\),稱 \(341\) 是以 \(2\) 為底的偽素數。\(341\) 是最小的偽素數基數。

\(a ^ {p - 1}\not \equiv 1\pmod p\),則 \(p\) 必然不是質數。多選幾個與 \(p\) 互質的數檢查,可以排除大部分合數。這被稱為費馬素性檢驗,它具有隨機性。

當面對形如 \(561\) 的卡邁克爾數時,費馬素性檢驗就相當劣了。卡邁克爾數 \(p\) 滿足所有與 \(p\) 互質的數的 \(p - 1\) 次方模 \(p\) 均為 \(1\)

1.2 二次探測定理

根據二次剩余部分的知識,當 \(p\) 為奇素數時,\(x ^ 2\equiv 1\pmod p\) 有且僅有解 \(\pm 1\)。因此,若存在 \(a \neq \pm 1\) 滿足 \(a ^ 2 \equiv 1\pmod p\),則 \(p\) 必然不是質數。

這被稱為 二次探測定理

1.3 算法介紹

結合費馬素性檢驗與二次探測定理。

根據二次探測定理,當 \(a ^ {p - 1} \equiv 1\pmod p\) 時,若 \(p - 1\)\(2\) 的倍數,則 \(a ^ {\frac {p - 1} 2}\) 必須是 \(\pm 1\)。若 \(\dfrac {p - 1} 2\) 仍是 \(2\) 的倍數且 \(a ^ {\frac {p - 1} 2}\equiv 1\pmod p\),則 \(a ^ {\frac {p - 1} 4}\) 必須是 \(\pm 1\),以此類推。

一般地,若 \(p - 1\)\(2 ^ r\) 的倍數且 \(a ^ {\frac {p - 1}{2 ^ {r - 1}}} \equiv 1\pmod p\),則 \(a ^ {\frac {p - 1}{2 ^ r}}\) 等於 \(\pm 1\),否則 \(p\) 不是素數。例如 \(a = 2\)\(p = 341\)\(2 ^ {340} \equiv 1 \pmod {341}\)\(2 ^ {170} \equiv 1\pmod {341}\),但 \(2 ^ {85} \equiv 32\pmod {341}\)。這說明 \(341\) 不是質數。

這樣檢測的准確率很高。隨機選擇 \(k\) 個底數,Miller-Rabin 算法的正確率(不會將合數誤判成素數的概率)大於 \(1 - 4 ^ {-k}\)。算法時間復雜度為 \(\mathcal{O}(k \log ^ 2 n)\)

Miller-Rabin 的效率與選取底數個數有關,我們希望減少底數並保證一定正確性。以下是常用底數,來自 wangrx 的博客。

  • \(2 ^ {32}\) 以內的數判素,使用 \(2, 7, 61\) 三個底數。
  • \(2 ^ {64}\) 以內的數判素,使用 \(2, 325, 9375, 28178, 450775, 9780504, 1795265022\) 七個底數。
  • 使用前 \(12\) 個素數作為底數可對 \(318665857834031151167460(3\times 10 ^ {23} \approx 2 ^ {78})\) 以內的數判素。詳見 A014233 - OEIS
  • 固定底數時需特判底數。若以 \(2, 7, 61\) 作為底數,則當 \(n = 2, 7\)\(61\) 時直接通過檢驗,因為 \(p\) 無法通過以 \(p\) 為底的費馬素性檢驗。

1.4 復雜度優化

Miller-Rabin 的復雜度和正確性足夠優秀,但注意到整個過程中我們多次使用快速冪計算 \(a\) 的冪,且指數每次除以 \(2\),很浪費。

考慮將整個過程反過來,即預先處理好 \(p - 1 = r \cdot 2 ^ d\),計算 \(a ^ r\) 並執行 \(d\) 次平方操作,即可得到每個 \(a ^ {r \cdot 2 ^ i}\)。時間復雜度優化為 \(\mathcal{O}(k\log n)\)

進一步地,先判掉 \(a ^ r\equiv 1\pmod p\),此時 \(p\) 通過檢驗。否則若 \(p\) 是質數,說明在 \(i\)\(d\) 減小到 \(0\) 的過程中,\(a ^ {r \cdot 2 ^ i}\bmod p\) 存在從 \(1\) 變為 \(-1\) 的過程,因此只需判斷是否存在 \(0\leq i < d\) 使得 \(a ^ {r \cdot 2 ^ i} \equiv -1 \pmod p\)。容易證明這是 \(p\) 通過本輪素性檢驗的充要條件。

代碼見 2.3 小節 P4718。

2. Pollard-Rho

分解質因數一般使用的試除法時間復雜度為 \(\mathcal{O}(\sqrt n)\),因為必須枚舉到 \(\sqrt n\) 才能確定 \(n\) 為質數。若預先篩出 \(\sqrt n\) 以內的質數,則復雜度除以 \(\log n\)

\(n\) 較小但數據組數 \(T\) 較多時,可預先使用線性篩 \(\mathcal{O}(n)\) 篩出值域內所有數的最小質因子,單次分解質因數 \(\mathcal{O}(\log n)\)

Pollard-Rho 為時間復雜度又開了一次平方,它可以在期望 \(\sqrt[4]{n}\log n\) 的時間復雜度內求出 \(n\) 的一個非平凡因子,因此使用 Pollard-Rho 分解質因數的時間復雜度為 \(\sqrt [4]{n}\log ^ 2 n\)

2.1 生日悖論

\(1\sim n\) 的正整數中 \(k\) 次等概率隨機選擇一個數,則所有數互不相同的概率為

\[P = \dfrac {n ^ {\underline{k}}} {n ^ k} \]

公式含義為從 \(n\) 個數中有序選擇 \(k\) 個互不相同的數的方案數除以總方案數。

  • 直觀認知:當 \(k\) 增大時,\(P\) 衰減很快,因為每次 \(\dfrac {n - k + 1} n\) 以相乘的方式作用在 \(P\) 上。可以理解為指數衰減,但比指數衰減慢。如下圖。

qttElD.png

手玩函數圖像后,我們發現使得 \(P = \dfrac 1 2\)\(k\) 似乎是 \(\sqrt n\) 級別的。

根據 \(1 + x \leq e ^ x\),可知 \(P \leq e ^ {\sum_{i = 1} ^ {k} \frac {n - i + 1} n - 1} = e ^ {-\frac{k(k - 1)}{2n}}\)。令 \(e ^ {-\frac{k(k - 1)}{2n}} = \dfrac 1 2\),解得 \(k\)\(\sqrt {n\ln 4}\) 附近。

因此,不嚴謹地,在 \(n\) 個數中等概率隨機選擇,使得選出的數中存在兩個相同的數的期望次數為 \(\mathcal{O}(\sqrt n)\)

2.2 算法介紹

首先,我們必須有能力快速判斷待分解的數是否為質數:Miller-Rabin。

Pollard-Rho 算法的精髓在於構造偽隨機函數 \(f(x) = x ^ 2 + c\)

\(f\) 僅含一個變量,故對於相同的 \(x\)\(f(x)\) 的返回值相同。在 \(f\) 進入循環前,它不斷迭代得到的數可視為隨機。其隨機性尚未被證明。

根據生日悖論,模 \(n\) 意義下,若給定 \(c\) 和初始值 \(x_0\),則 \(f\) 在不斷迭代的過程中期望迭代 \(\mathcal{O}(\sqrt {n})\) 次進入長度為 \(\mathcal{O}(\sqrt {n})\) 的循環。一般以 \(0\) 作為初始值,則迭代過程形如 \(x_1 = f(x_0) = c\)\(x_2 = f(x_1) = c ^ 2 + c\)\(\cdots\)\(x_i = f ^ i(x_0)\)

因為整條路徑類似希臘字母 \(\rho\),算法得名 Pollard-Rho,如下圖。

\(n\) 為合數,則其最小非平凡因子 \(m\) 不超過 \(\sqrt n\)。因此模 \(m\) 意義下期望迭代 \(\mathcal{O}(\sqrt [4]{n})\) 次進入長度為 \(\mathcal{O}(\sqrt [4]{n})\) 的循環。

同時,若模 \(m\) 一旦進入循環即存在 \(x_i \equiv x_j\pmod m\),即可通過計算 \(|x_i - x_j|\)\(n\)\(\gcd\) 求出 \(m\) 或其倍數,前提為 \(x_i \not\equiv x_j\pmod n\),否則 \(\gcd\) 結果為 \(n\),平凡。

\(x_i\) 在模 \(n\) 意義下形成的路徑為 \(\rho_n\),在模 \(m\) 意義下形成的路徑為 \(\rho_m\)。問題為求出一組 \(i, j\) 使得其處於 \(\rho_m\) 同一點,但處於 \(\rho_n\) 不同點。這說明 \(i\) 需要足夠大使得跳出 \(\rho_m\) 的尾巴,且 \(j - i\) 恰好是 \(\rho_m\) 循環節的倍數。據分析,最小的 \(i\)\(j\) 的級別均為 \(\mathcal{O}(\sqrt [4]{n})\)

注意,整個過程中我們 不知道 \(m\),但分析可證求 \(m\) 的期望復雜度為 \(\mathcal{O}(\sqrt [4]{n}\log n)\)。求得的非平凡因子 \(m\) 不一定最小,因此還需繼續分解。

考慮從 \(i = 1\) 開始計算 \(d = \gcd(|x_{2i} - x_i|, n)\) 直到該值不等於 \(1\),此時有兩種情況:

  • \(d < n\),說明 \(d\)\(n\) 的非平凡因子,直接返回。
  • \(d = n\),說明進入 \(\rho_n\) 的循環節,大概率因為 \(\rho_n\) 的環長等於 \(\rho_m\) 的環長,也可能因為 \(\rho_m\) 的尾巴過長,使得第一次枚舉到 \(\rho_m\) 環長的倍數使得 \(i\) 跳出 \(\rho_m\) 的尾巴時就枚舉到了 \(\rho_n\) 的環長。無論如何,應結束本次失敗的 Pollard-Rho,調整參數 \(c\) 重新分解。

上述算法稱為基於 Floyd 判環的 Pollard-Rho 算法,期望時間復雜度 \(\mathcal{O}(\sqrt [4]{n} \log n)\)

  • 注意:筆者實現 Floyd 判環后,發現若令 \(d = \gcd(|x_{2i} - x_i|, n)\)\(n = 4\) 無論 \(c\) 取何值均無法分解。需要特判 \(n = 4\) 或從 \(i = 0\) 開始計算 \(d = \gcd(|x_{2i + 1} - x_i|, n)\)
  • 優化:\(\gcd\) 次數過多降低效率。朴素 Floyd 判環法無法通過模板題。考慮設置 樣本累計上限 \(T\),將 \(T\)\(\gcd\) 測試打包,將常數減小 \(T\) 倍。\(T = 10\) 在模板題數據下表現優秀。其正確性基於 \(\gcd(a, n) \mid \gcd(ab\bmod n, n)\)
  • 優化:二分未知上界的數的最好方法是倍增。\(n\) 較大時 \(T = 10\) 太小了。考慮每檢查一次就將 \(T\) 乘以 \(2\)。同時為防止 \(T\) 過大無法及時檢查,令 \(T\)\(C\) 取較小值。一般取 \(C = 127\)

如果讀者擔心 \(n\) 較小時算法的正確性,可預處理答案。讀者需要認識到 PR 本身是隨機性算法的事實,沒有絕對正確的寫法,只有效率和正確性相對優秀的寫法。對於廣為流傳的 PR 寫法,前人已經驗證其在一定范圍內的正確性,可放心大膽使用。

總之,\(d\) 的計算方式多種多樣,但樣本累計優化不可少。檢查太耗時則打包,這樣的思想不僅可用於優化 Pollard-Rho,也可以應用在其它領域。

2.3 例題

P4718【模板】Pollard-Rho 算法

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
mt19937 rnd(chrono::steady_clock::now().time_since_epoch().count());
ll rd(ll l, ll r) {return rnd() % (r - l + 1) + l;}
ll ksm(ll a, ll b, ll p) {
  ll s = 1;
  while(b) {
    if(b & 1) s = (__int128) s * a % p;
    a = (__int128) a * a % p, b >>= 1;
  }
  return s;
}
bool Miller(ll n) {
  if(n < 3 || n % 2 == 0) return n == 2;
  ll r = n - 1, d = 0;
  while(r & 1 ^ 1) r >>= 1, d++;
  for(int _ = 0; _ < 10; _++) {
    ll a = rd(2, n - 1), v = ksm(a, r, n);
    if(v == 1) continue;
    for(int i = 0; i <= d; i++) {
      if(i == d) return 0;
      if(v == n - 1) break;
      v = (__int128) v * v % n;
    }
  }
  return 1;
}
ll Pollard(ll n) {
  ll c = rd(1, n - 1), s = c, t = 0;
  auto f = [&](ll x) {return ((__int128) x * x + c) % n;};
  ll acc = 0, prod = 1, d, limit = 1;
  while(s != t) {
    prod = (__int128) prod * abs(s - t) % n;
    if(++acc == limit) {
      if((d = __gcd(prod, n)) > 1) return d;
      acc = 0, limit = min(127ll, limit << 1);
    }
    s = f(f(s)), t = f(t);
  }
  if((d = __gcd(prod, n)) > 1) return d;
  return n;
}
ll mxp(ll n) {
  if(Miller(n)) return n;
  if(n == 1) return 1;
  ll d = Pollard(n);
  while(d == n) d = Pollard(n);
  while(n % d == 0) n /= d;
  return max(mxp(d), mxp(n));
}
int main() {
  ios::sync_with_stdio(0);
  ll T, n;
  cin >> T;
  while(T--) {
    cin >> n;
    if(Miller(n)) cout << "Prime\n";
    else cout << mxp(n) << "\n";
  }
  return 0;
}

參考資料

第一章:

第二章:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM