天天看點

篩法求大量素數

一、樸素篩法求n以内的所有素數

原理:如果一個數是合數,那麼我們可以用它的因子(不包括1與它自身)把這個數篩掉。

思路:

         bool f[maxn+100];

         f[i]==1表示數字i不是質數,f[i]==0表示數字i是質數 

         int p[maxn];

         p[0]表示素數的個數,p[i]記錄第i個素數的值  

         我們從2開始往後進行掃描,假設目前掃描到數字 i ,有兩種情況:

          1、f[i]==1,即表示數字 i 為合數,不作處理。

          2、f[i]==0,即表示數字 i 為質數。這是為什麼呢?根據上述原理,若存在數字 j (1<j<i ) 為 i 的因子,那麼在前面枚舉到 j 時,我們會把 j 的所有倍數都标記為合數,也就是說,f[i]==0 就表示在小于 i 的數中,不存在 i 的因子 j ,數字 i 當然也就是質數了。

                ok,接下來還有兩步操作: 

                首先,我們要将素數 i 存儲下來;

                其次,将素數 i 的所有倍數都标記為合數。          

代碼:

f[0]=f[1]=1;
  for(i=2;i<=n;i++)
    if(f[i]==0)
      {
        p[++p[0]]=i;
        for(j=i+i;j<=n;j+=i)f[j]=1;
      }      

二、線性篩法求n以内的素數

原理:每個合數都有一個最小的質因數,每個合數隻被它的最小質因數篩去。

思路:分析上述的樸素篩法,我們可以發現一個數實際上可能被多次篩選,比如數字12,被數字2、3篩選兩次,這就造成效率上的               浪費,有沒有什麼辦法可以使得每個數字都隻被篩選一次,進而使得此篩法成為線性的呢?

            我們可以将樸素篩法進行改進,使得每個數字都隻被它的最小質因數篩去。

            (狀态定義與上面的一緻)

            假設目前枚舉到數字 i ,接下來有兩步處理:

            1、若f[i]==0,即數字 i 為質數,那麼将數字 i 假如素數表p。

            2、此時的話,素數表 p 記憶體儲的都是小于等于 i 的素數。假設第 j  個素數為 p[j] ,則 k =p[j]*i,k的最小質因數即為 p[j],我們就用 p[j] 篩去數字 k。

                好了,這裡還要講一下的是,下列代碼中的這一句:

                          i f ( i % p [ j ] ==0 ) break;

                為什麼要加這一句呢?若 i % p[j]==0 表示 i 是 p[j] 的倍數,則 k=p[j+1]*i也是 p[j] 倍數,并且可以将其變形得到這樣的一個式                 子:k = p[j+1]*i = p[j+1] * p[j] * x。

                我們的方法是隻用一個數的最小質因數篩去它,由此,p[j] 之後的素數就沒有再枚舉的必要了,而數字p[j] * i 會在之後枚舉                   到數字 p[j+1] * x 時,被  p[j] 篩掉。  

代碼:

int i,j,k,n;
f[0]=f[1]=1;
p[0]=0;
for(i=2;i<=n;i++)
  {
    if(!f[i])p[++p[0]]=i;
    for(j=1;j<=p[0] && i*p[j]<=n;j++)
      {
        f[i*p[j]]=1;
        if(i%p[j]==0)break;
      }
  }      

看了上述的線性篩法,其實還能發現線性求解 n 以内每個數的最小質因數的解法,至于怎麼做,這裡就不再多說了。不知道的話,可以在下方留言。

三、空間折半優化

原理:所有的偶數(除 2 以外)都為合數。

思路:在前面的狀态定義中,我們都是直接存儲數字 i ,用 f[i] 來記錄數字 i 是否為質數。根據上述原理,我們可以隻記錄奇數,而               不記錄偶數,這樣的話,對于給定的範圍 n ,我們原本需要定義長度為 n 的數組,現在僅僅隻用定義長度為 n/2 的數組即可。

            f[i] 記錄數字2*i+1是否為質數,其餘的處理與上述的篩法類似。

加入空間優化之後的代碼:

int i,j,k,n,maxn;
  maxn=n/2;
  p[++p[0]]=2;
  for(i=1;i<=maxn;i++)
    {
      k=2*i+1;  
      if(!f[i])p[++p[0]]=k;
      for(j=2;j<=p[0] && k*p[j]/2<=maxn;j++)
        {
          f[k*p[j]/2]=1;
          if(k%p[j]==0)break;
        }
    }      

四、區間求素數

簡述:有的時候,我們需要知道某個特定區間的素數(區間大小較小,但數可能很大)。

那麼數組就開不下,這時候我們仍然可以使用篩法,隻是所有的下标都進行了偏移。

大家了解下面這段代碼可以先用普通篩法寫,然後數組下标集體移動即可。

const int maxn = 100000;

int PrimeList[maxn];

int PrimeNum;

bool IsNotPrime[maxn]; // IsNotPrime[i] = 1表示i + L這個數是素數.

void SegmentPrime(int L, int U)

{ // 求區間[L, U]中的素數.

int i, j;

int SU = sqrt(1.0 * U);

int d = U - L + 1;

for (i = 0; i < d; i++) IsNotPrime[i] = 0; // 一開始全是素數.

for (i = (L % 2 != 0); i < d; i += 2) IsNotPrime[i] = 1; // 把偶數的直接去掉.

for (i = 3; i <= SU; i += 2)

{

  if (i > L && IsNotPrime[i - L]) continue; // IsNotPrime[i - L] == 1說明i不是素數.

  j = (L / i) * i; // j為i的倍數,且最接近L的數.

  if (j < L) j += i;

if (j == i) j += i; // i為素數,j = i說明j也是素數,是以直接 + i.

j = j - L;

for (; j < d; j += i) IsNotPrime[j] = 1; // 說明j不是素數(IsNotPrime[j - L] = 1).

}

if (L <= 1) IsNotPrime[1 - L] = 1;

if (L <= 2) IsNotPrime[2 - L] = 0;

PrimeNum = 0;

for (i = 0; i < d; i++) if (!IsNotPrime[i]) PrimeList[PrimeNum++] = i + L;

}