素數算法


一、引言

在平時做題目或者進行運算的時候,素數的出現次數總是十分頻繁。這里總結了一些常見的判定素數和計算某個范圍內素數個數的一些算法。部分代碼來源於 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也不是。
這個方法也可以稱作試除法。

 

 

三、篩選法

上面介紹的一些素數判斷的算法,在某些問題是基本可以適用的了。但是對於另外一類問題卻十分尷尬。比如問你整數區間[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的模板。

四、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;
}


五、容斥原理

從上面的代碼可以發現,顯然這種篩法只能應付達到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])以此類推...於是就可以得到正確結果了。

代碼如下:



六、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;
}
}

 


免責聲明!

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



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