素數判定演算法 初級

qiyuewuyi2333發表於2024-05-28

前置知識

Cpp實現

基礎演算法

// base method
bool basement(int num)
{
	for (int i = 2; i <= sqrt(num); ++i)
	{
		if (num % i == 0)
			return false;
	}
	return true;
}

證明

篩法初步

根據初等數學的知識,如果一個數不是2的倍數,那麼它肯定不是2的倍數的倍數,所以,進一步的我們可以對上面的基礎演算法進行最佳化

// sieve first step
bool sieve2Method(int num)
{
	if (num == 2)
		return true;
	if (num % 2 == 0 || num < 2)
		return false;
	else
	{
		for (int i = 3; i * i <= num; i += 2)
		{
			if (num % i == 0)
			{
				return false;
			}
		}
		return true;
	}
}

輪轉篩法

6k ± 1 形式輪換篩法(輪轉篩法)(Wheel Factorization)。

輪轉篩法的基本原理是利用模數(在這裡是6)的性質來減少需要檢查的數。具體到6k ± 1形式,這個形式背後的理由如下:

  • 整數 n 可以表示為 6𝑘+𝑟,其中 𝑟 是0到5之間的一個整數。
  • 對於 𝑟=0,2,3,4,這些數都可以被2或3整除(即它們是合數)。
  • 只有 𝑟=1 和 𝑟=5(即 6𝑘+1 和 6𝑘−1)可能是質數。
bool isPrime_3(int num)
{
	if (num == 2 || num == 3)
		return 1;
	// 不在6的倍數兩側的一定不是質數
	if (num % 6 != 1 && num % 6 != 5)
		return 0;
	int tmp = sqrt(num);
	// 在6的倍數兩側的也可能不是質數
	for (int i = 5; i <= tmp; i += 6)
		if (num % i == 0 || num % (i + 2) == 0)
			return 0;
	// 排除所有,剩餘的是質數
	return 1;
}

埃拉託斯特尼篩法生成素數表

根據上面我們的初步想法,我們可以進一步的將用於篩選的因子擴大。
但是,這種篩法的核心思想之一是:
如何確定篩選因子
既然我們要做到高效,那麼這些篩選因子之間的篩取最好沒有重合,或者重合度很小,至少它不應該完全重複篩取,對吧?
考慮2,3,4這三個數。
經過簡單運算,我們知道將3作為篩選因子,是可以篩取到2曬不出的數字的,比如說9,但是4,因為它有因子2,所以它所有篩取的數字,均早就被2篩取過了。
所以,我們應該選取素數作為篩取因子。

std::vector<bool> sieveOfEratosthenes(int n)
{
	std::vector<bool> isPrime(n + 1, true);
	isPrime[0] = isPrime[1] = false; // 0和1不是素數

	for (int p = 2; p <= std::sqrt(n); ++p)
	{
		if (isPrime[p])
		{
			for (int i = p * p; i <= n; i += p)
			{
				isPrime[i] = false;
			}
		}
	}
	return isPrime;
}

但是這裡面還有一些實現細節,需要注意:

  • 初始化0 1 索引為false,
  • p <= sqrt(n)
  • i = p * p

我們一個個來說,1 略
2 為什麼p<=sqrt(n),這樣可以篩全嗎?
是可以的,首先我們初始化值為false,這意味著我們只需要篩選出 1 ~ n中的合數即可。
又根據我們上面對於基本方法的迴圈範圍的證明,所以,只要一個數是合數,那麼它肯定會在2~ $\sqrt{ n }$ 之間
所以,我們可以透過反向推導,如果某一個因子,能夠透過倍加自己,或者可以理解為以自己為步長進行步進,那麼他肯定能夠到達那些以它為因子的合數位置上

3 為什麼 內層的i要初始化為 $p * p$ ,而不是 $p * 2$之類的
這是因為要防止和之前已經篩過的部分發生重合,比如3個2和2個3

尤拉篩法

從上面埃氏篩法,我們確立了可以透過篩取合數,從而反向獲取素數的思路。但顯然,它仍有最佳化的空間,那就是重複的篩取。而尤拉篩法正為此而生。

尤拉篩,又稱線性篩,時間複雜度只有O(n)

在埃氏篩法的基礎上,讓每一個合數都只被它的最小質因子篩選一次,以達到不重複篩選的目的,大大地節省了時間,從埃氏篩的O(n2)降到O(n)級別

我們想要阻止重複標記的發生,就需要一種規則,也就是說只讓標記以某一種特定的形式or規律被標記,在尤拉篩法中,這表現為,只用最小素因子去標記

為了知道最小素因子,我們很自然地需要一個表維護已知的素數

尤拉篩法正確性的證明

實現

vector<int> eulerSieve(int n)
{
	std::vector<bool> isPrime(n + 1, true);
	std::vector<int> primes;         // 素數集合
	isPrime[0] = isPrime[1] = false; // 0和1不是素數

	for (int i = 2; i <= n; ++i)
	{
		if (isPrime[i])
		{
			primes.push_back(i);
		}
		for (int j = 0; j < primes.size() && i * primes[j] <= n; ++j)
		{
			isPrime[i * primes[j]] = false;
			if (i % primes[j] == 0)
				break;
		}
	}
	return primes;
}

Miller-Rabin演算法。
暫時不看~

Miller-Rabin演算法

Miller-Rabin演算法是一種機率性質數測試演算法,可以用來判斷一個大整數是否為質數。該演算法基於數論中的一些深刻性質,其優點在於對大數的判斷效率非常高。雖然它是一個機率演算法,但透過多次測試,可以將錯誤率降到非常低。

Miller-Rabin演算法步驟

Miller-Rabin演算法基於Fermat小定理以及以下兩個重要的數學性質:

  1. 如果 𝑛 是一個質數,則對於任何整數 𝑎 滿足 $1≤𝑎≤𝑛−1$,有 $𝑎^{n-1} ≡ 1 mod  𝑛$。
  2. 如果 𝑛 是一個奇質數,則存在一個唯一的表示式 $𝑛−1=2^{s}⋅𝑑$,其中 𝑑 是一個奇數,$𝑠≥1$。

具體步驟

  1. 將 𝑛−1 表示為 $2^{s}⋅𝑑$:

    • 例如,對於 𝑛=15n=15,我們有 𝑛−1=14n−1=14,即 14=2⋅714=2⋅7,這裡 𝑑=7d=7 和 𝑠=1s=1。
  2. 隨機選擇一個整數 𝑎 其中$1 \le a \le n-1$

    • 如果存在 $𝑎𝑑≡1mod  𝑛$,則 𝑛n 可能是一個質數。
    • 對於 𝑗=0,1,…,𝑠−1,如果存在 $𝑎{2𝑗⋅𝑑}≡−1mod  𝑛$,則 𝑛 可能是一個質數。
  3. 重複上述測試 k 次:

    • 選擇不同的 𝑎 進行多次測試。
    • 如果所有測試均透過,則 𝑛 很可能是一個質數。
    • 如果有一次測試失敗,則 𝑛 不是質數。

Miller-Rabin演算法的虛擬碼

#include <iostream>
#include <cstdlib>
#include <ctime>

// 使用快速冪演算法計算 (base^exponent) % mod
long long mod_exp(long long base, long long exponent, long long mod) {
    long long result = 1;
    base = base % mod;
    while (exponent > 0) {
        if (exponent % 2 == 1) {
            result = (result * base) % mod;
        }
        exponent = exponent >> 1;
        base = (base * base) % mod;
    }
    return result;
}

// Miller-Rabin測試的核心函式
bool miller_test(long long d, long long n) {
    long long a = 2 + rand() % (n - 4); // 隨機選擇 2 <= a <= n-2
    long long x = mod_exp(a, d, n);

    if (x == 1 || x == n - 1) {
        return true;
    }

    while (d != n - 1) {
        x = (x * x) % n;
        d *= 2;

        if (x == 1) {
            return false;
        }
        if (x == n - 1) {
            return true;
        }
    }
    return false;
}

// Miller-Rabin 素性測試
bool is_prime(long long n, int k) {
    if (n <= 1 || n == 4) {
        return false;
    }
    if (n <= 3) {
        return true;
    }

    // 將 n-1 表示為 2^s * d
    long long d = n - 1;
    while (d % 2 == 0) {
        d /= 2;
    }

    // 進行 k 次測試
    for (int i = 0; i < k; i++) {
        if (!miller_test(d, n)) {
            return false;
        }
    }
    return true;
}

int main() {
    srand(time(0)); // 初始化隨機數生成器

    long long n;
    int k = 5; // 測試次數
    std::cout << "Enter a number to check if it is prime: ";
    std::cin >> n;

    if (is_prime(n, k)) {
        std::cout << n << " is a prime number." << std::endl;
    } else {
        std::cout << n << " is not a prime number." << std::endl;
    }

    return 0;
}

程式碼解析

  1. 快速冪演算法mod_exp函式用於計算 (𝑏𝑎𝑠𝑒𝑒𝑥𝑝𝑜𝑛𝑒𝑛𝑡)mod  𝑚𝑜𝑑(baseexponent)modmod,以高效地進行大數冪運算。
  2. Miller-Rabin測試的核心函式miller_test函式進行一次Miller-Rabin測試,透過隨機選擇基數 𝑎 並進行多次平方檢驗來判斷 𝑛 是否可能是質數。
  3. 素性測試函式is_prime函式呼叫 miller_test 函式進行多次測試,以機率性的方式判斷 𝑛n 是否為質數。

Miller-Rabin演算法的優點

  • 高效:對於大數,Miller-Rabin測試比許多其他演算法更高效。
  • 可調性:透過增加測試次數 𝑘,可以降低誤判率,使得演算法在實際應用中非常可靠。

相關文章