素數算法

一、引言

在平時做題目或者進行運算的時候,素數的出現次數總是十分頻繁。這裏總結了一些常見的判定素數和計算某個範圍內素數個數的一些算法。部分代碼來源於 kuangbin 的模板,嗯畢竟都是跟着這個學的...


二、樸素判斷素數算法

就判斷素數而言,事實上是非常簡單的了。根據定義,判斷一個整數n是否是素數,只需要去判斷在整數區間[2, n-1]之內,是否具有某個數m,使得n % m == 0。代碼可以這麼寫:

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

事實上,這個算法是O(n)的,感覺是很快了,但是依舊無法滿足需求。所以有一個算法是O(sqrt(n))的算法。代碼可以這麼寫:

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

原理很巧妙,也僅僅是把代碼的i < n變成了i * i <= n而已,但是優化卻是極其高的。可能你會注意到,在上一份代碼裏面,我定義的n爲int類型,而後面一份代碼,我卻定義成了long long,這是因爲在1s內,上一份代碼能判斷出來的數量級爲1e8,而後面一份代碼判斷出來的數量級卻幾乎可以達到1e16。而原因僅僅是因爲a * b = c的話一旦a是c的約數,那麼b也是,如果a不是,那麼b也不是。

這個方法也可以稱作試除法。


三、Miller_Rabin素性測試

儘管上面的O(sqrt(n))的算法已經非常優秀了,但是面對更高數量級的“大數”卻會顯得力不從心。而這個時候,Miller_Rabin的優越性就顯示出來了。

Miller_Rabin的理論基礎來源於費馬小定理。值得一提的是費馬小定理是數論四大定理之一。

費馬小定理:n是一個奇素數,a是任何整數(1≤ a≤n-1) ,則 a^(n-1)≡1(mod n)

要測試n是否是一個素數,首先將n-1分解爲(2^s) * d,在每次測試開始時,先隨機選擇一個介於[1, N - 1]的整數a,之後如果對於所有的r∈[0, s - 1],若a^d mod N ≠ 1且a^((2^r) * d) mod N ≠ -1,那麼n就是一個合數,否則n有3/4的機率是素數。

這也是爲什麼說這個算法只是素性測試了,他不能完全保證結果正確,但是當測試次數大約爲20的時候,基本可以確保正確率達到97%以上。

它的代碼可以這麼寫:

const int S = 20;
LL mod_mul(LL a, LL b, LL n) {
	LL res = 0;
	while (b) {
		if (b & 1) res = (res + a) % n;
		a = (a + a) % n;
		b >>= 1;
	}
	return res;
}
LL mod_exp(LL a, LL b, LL n) {
	LL res = 1;
	while (b) {
		if (b & 1) res = mod_mul(res, a, n);
		a = mod_mul(a, a, n);
		b >>= 1;
	}
	return res;
}
bool miller_rabin(LL n) {
	if (n == 2 || n == 3 || n == 5 || n == 7 || n == 11) return true;
	if (n == 1 || !(n % 2) || !(n % 3) || !(n % 5) || !(n % 7) || !(n % 11)) return false;

	LL x, pre, u = n - 1, k = 0;

	while (!(u & 1)) {
		++k;
		u >>= 1;
	}

	srand((LL)time(NULL));
	for (int i = 0; i < S; ++i) {                     //進行S次測試,S越大,結果越準確
		x = rand() % (n - 2) + 2;                   //在[2, n)中取隨機數
		if (x % n == 0) continue;

		x = mod_exp(x, u, n);                       //計算x^u % n
		pre = x;
		for (int j = 0; j < k; ++j) {
			x = mod_mul(x, x, n);
			if (x == 1 && pre != 1 && pre != n - 1)
				return false;
			pre = x;
		}
		if (x != 1) return false;
	}
	return true;
}

四、篩選法

上面介紹的一些素數判斷的算法,在某些問題是基本可以適用的了。但是對於另外一類問題卻十分尷尬。比如問你整數區間[1, n]內的素數個數是多少。這個時候如果一個個枚舉,一個個判斷,對於樸素方法來說,最優也是O(nsqrt(n)),即使是Miller_Rabin算法也無法在O(n)的時間內得到結果。於是就有了埃氏篩選法(埃拉託斯特尼篩法)。

對於篩選整數n以內的素數,算法是這麼描述的:先把素數2的倍數全部刪除,剩下的數第一個爲3,再把素數3的倍數全部刪除,剩下的第一個數爲5,再把素數5的倍數全部刪除······直到把n以內最後一個素數的倍數刪除完畢,得到n以內的所有素數。代碼可以這麼寫:

const int maxn = 1e7 + 5;
int pri[maxn];
void getPrime(int n) {
	for (int i = 0; i <= n; ++i) pri[i] = i;
	pri[1] = 0;
	for (int i = 2; i <= n; ++i) {
		if (!pri[i]) continue;
		pri[++pri[0]] = i;
		for (int j = 2; i * j <= n && j < n; ++j) {
			pri[i * j] = 0;
		}
	}
}

而事實上,觀察可以發現,上面的這種寫法有很多次重複計算,這樣顯然無法做到線性篩選,而另外一種寫法卻可以得到線性篩選,達到時間複雜度爲O(n),代碼可以這麼寫:

const int MAXN = 1e7 + 5;
void getPrime(){
	memset(prime, 0, sizeof(prime));
	for (int i = 2;i <= MAXN;i++) {
		if (!prime[i])prime[++prime[0]] = i;
		for (int j = 1;j <= prime[0] && prime[j] <= MAXN / i;j++) {
			prime[prime[j] * i] = 1;
			if (i%prime[j] == 0) break;
		}
	}
}

來自kuangbin的模板。


五、容斥原理

從上面的代碼可以發現,顯然這種篩法只能應付達到1e7這種數量級的運算,即使是線性的篩選法,也無法滿足,因爲在ACM競賽中,1e8的內存是極有可能獲得Memery Limit Exceed的。

於是可以考慮容斥原理。

以AHUOJ 557爲例,1e8的情況是篩選法完全無法滿足的,但是還是考慮a * b = c的情況,1e8只需要考慮10000以內的素數p[10000],然後每次先減去n / p[i],再加上n / (p[i] * p[j])再減去n / (p[i] * p[j] * p[k])以此類推...於是就可以得到正確結果了。

代碼如下:

#include <cmath>
#include <cstdio>
using namespace std;
 
const int maxn = 10005;
int sqrn, n, ans = 0;
bool vis[maxn];
int pri[1500] = {0};
void init(){
    vis[1] = true;
    int k = 0;
    for(int i = 2; i < maxn; i++){
        if(!vis[i]) pri[k++] = i;
        for(int j = 0; j < k && pri[j] * i < maxn; j++){
            vis[pri[j] * i] = true;
            if(i % pri[j] == 0) break;
        }
    }
}
void dfs(int num, int res, int index){
    for(int i = index; pri[i] <= sqrn; i++){
        if(1LL * res * pri[i] > n){
            return;
        }
        dfs(num + 1, res * pri[i], i+1);
        if(num % 2 == 1){
            ans -= n / (res * pri[i]);
        }else{
            ans += n / (res * pri[i]);
        }
 
        if(num == 1) ans++;
    }
}
int main(){
    init();
    while(~scanf("%d",&n) && n){
        ans = n;
        sqrn = sqrt((double)n);
        dfs(1,1,0);
        printf("%d\n",ans-1);
    }
    return 0;
}

六、Meissel-Lehmer算法

最後介紹的這個算法可以說是黑科技級別的,它能夠在3s內求出1e11之內的素數個數。據說還有在300ms內求出1e11的個數的。可以參考wiki裏面原理。然後給出來自Codeforces 665F題目裏面的代碼。

#define MAXN 100    // pre-calc max n for phi(m, n)
#define MAXM 10010 // pre-calc max m for phi(m, n)
#define MAXP 40000 // max primes counter
#define MAX 400010    // max prime
#define setbit(ar, i) (((ar[(i) >> 6]) |= (1 << (((i) >> 1) & 31)))) 
#define chkbit(ar, i) (((ar[(i) >> 6]) & (1 << (((i) >> 1) & 31))))
#define isprime(x) (( (x) && ((x)&1) && (!chkbit(ar, (x)))) || ((x) == 2))

namespace pcf {
	long long dp[MAXN][MAXM];
	unsigned int ar[(MAX >> 6) + 5] = { 0 };
	int len = 0, primes[MAXP], counter[MAX];

	void Sieve() {
		setbit(ar, 0), setbit(ar, 1);
		for (int i = 3; (i * i) < MAX; i++, i++) {
			if (!chkbit(ar, i)) {
				int k = i << 1;
				for (int j = (i * i); j < MAX; j += k) setbit(ar, j);
			}
		}

		for (int i = 1; i < MAX; i++) {
			counter[i] = counter[i - 1];
			if (isprime(i)) primes[len++] = i, counter[i]++;
		}
	}

	void init() {
		Sieve();
		for (int n = 0; n < MAXN; n++) {
			for (int m = 0; m < MAXM; m++) {
				if (!n) dp[n][m] = m;
				else dp[n][m] = dp[n - 1][m] - dp[n - 1][m / primes[n - 1]];
			}
		}
	}

	long long phi(long long m, int n) {
		if (n == 0) return m;
		if (primes[n - 1] >= m) return 1;
		if (m < MAXM && n < MAXN) return dp[n][m];
		return phi(m, n - 1) - phi(m / primes[n - 1], n - 1);
	}

	long long Lehmer(long long m) {
		if (m < MAX) return counter[m];

		long long w, res = 0;
		int i, a, s, c, x, y;
		s = sqrt(0.9 + m), y = c = cbrt(0.9 + m);
		a = counter[y], res = phi(m, a) + a - 1;
		for (i = a; primes[i] <= s; i++) res = res - Lehmer(m / primes[i]) + Lehmer(primes[i]) - 1;
		return res;
	}
}

七、後記

寫這麼多...發現我會的也就這麼點了....找了不少的模板...嘛...不過把這些知識梳理一遍,思路也變得清晰許多。以後繼續加油~~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章