1. 程式人生 > 實用技巧 >你能告訴我一億以內有多少對孿生素數嗎?

你能告訴我一億以內有多少對孿生素數嗎?

所謂孿生素數,就是相差為2的素數對,例如3和5,11和13。如果僅僅是100以內的孿生素數,相信大部分人只用數就能數出來,畢竟100以內只有25個素數。但是如果是1000以內呢?100000以內呢?如果像題目中說的一樣,一億以內呢?

硬著頭皮數顯然不行了,要解決這個問題,我們要依賴於程式設計。


要求孿生素數的對數,首先要找到孿生素數,要找到孿生素數,首先要找到素數。C++中有許多找素數的方法,比如基礎的試除法,其程式碼如下:

bool prime(int n) 
{
    if(n<2)  return false;
    for(int i=2;i*i<=n;i++)
    {
        
if(n%i==0) return false; } return true; }

這段程式碼比較基礎,也很容易理解。

完整程式如下:

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int n,cnt; //cnt記錄孿生素數對數 
bool prime(int n)  //試除法篩素數 
{
    if(n<2)  return false;
    for(int i=2;i*i<=n;i++)
    {
        
if(n%i==0) return false; } return true; } int main() { scanf("%d",&n); for(int i=2;i<=n;i++) { if(prime(i)&&prime(i+2)) //判斷是否滿足孿生素數定義 { cnt++; } } printf("%d\n",cnt);return 0; }

一切都很順利的進行了,我們不禁暗想:

孿生素數,就這???

然而,當輸入“100000000”時,奇怪的事情發生了,答案久久沒有出現在小黑板上,只有游標在閃動著寂寞的白光,宛若孤獨而無人陪伴的我

這令人疑惑,於是我關閉了視窗,重新執行,並輸入了較小的數。答案几乎是在敲回車後的一剎那出現在小黑板上。

這說明程式沒有問題,輸入“100000000”答案遲遲不出現,只有一個可能——程式在運算結果。

既然如此,那我們能做的就只有等待。

終於,在不知多久之後,小黑板上終於出現了我們所希望看到的東西——440312。

雖然得到了結果,但比起這個,我們更想知道它到底算了多久,於是我在程式中加入了從百度抄來的計時程式,如下:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<time.h>  //標頭檔案 
using namespace std;
clock_t start,finish;  //定義始終 
double duration;  //定義時間 
int n,cnt; 
bool prime(int n)  
{
    if(n<2)  return false;
    for(int i=2;i*i<=n;i++)
    {
        if(n%i==0)  return false;
    }
    return true;
}
int main()
{
    scanf("%d",&n);
    start=clock();  //在程式開始執行時開始計時,注意,若將這句話加在輸入之前,會把輸入資料的時間記入,影響結果 
    for(int i=2;i<=n-2;i++)
    {
        if(prime(i)&&prime(i+2))  
        {
            cnt++;
        }
    }
    finish=clock();  //結束計時 
    duration=(double)(finish - start)/CLOCKS_PER_SEC;  //計算時間 
    printf( "%f seconds\n",duration);  //輸出時間    
    printf("%d\n",cnt);return 0;
}

當輸入“10000”,運算時間為0.002000s,輸入“1000000”,運算時間為0.310000s,兩次運算時間並沒有差很多。

但輸入“100000000”,在又一次漫長的等待後,小黑板上出現了驚人的149.797000s,是計算1000000以內的孿生素數對數所用時間的約483倍。

這可怕的數字令我們感到恐懼,若是資料範圍再大一些,試除法豈不是要算一年!

看來資料太大,用試除法求解是行不通了,我們需要的是效率更高的演算法。


提到高效演算法,聰明的你一定能想到Eratosthenes篩選法,翻譯成人話就是埃氏篩。

埃氏篩的基本思想:素數的倍數一定不是素數。先假設所有數都是素數,從小到大列舉每一個素數x,把x的倍數都標記為非素數。當從小到大掃描到一個數x時,若它尚未被標記,則它不能被2~x-1之間的任何數整除,該數就是素數。(對整數1特殊處理)

埃氏篩程式碼如下:

void primes(int n)
{
    memset(v,0,sizeof(v)); //合數標記 
    for(int i=2;i<=n;i++)
    {
        if(v[i])  continue;    
        cout<<i<<endl;  //i是素數 
        for(int j=i;j<=n/i;j++)
        {
            v[i*j]=1;
        }      
    }
}

埃氏篩的時間複雜度是O(nloglogn),效率非常接近線性,是一種常用的素數篩法。然而,埃氏篩會對合數進行重複標記,即使是優化之後,其根本原因是演算法不能唯一確定產生合數的方式。據此,我們在生成一個需要標記的合數時,每次只向現有的數乘上一個質因子,並且讓它是這個合數的最小質因子。這相當於讓合數的質因子從大到小累積。具體來說,我們採用如下的篩法:

int v[maxn],prime[maxn]; 
void primes (int n) //用線性篩找素數 
{
    memset(v,0,sizeof(v)); //最小質因子 
    m=0; //素數數量 
    for(int i=2;i<=n;i++)
    {
        if(!v[i]) //i是質數 
        {
            v[i]=i;
            prime[++m]=i;
        }
                            //給當前的數i乘上一個質因子 
        for(int j=1;j<=m;j++)
        { 
                            //i有比prime[j]更小的質因子,或者超出n的範圍 
            if(prime[j]>v[i]||prime[j]>n/i)  break;
                            //prime[j]是合數i*prime[j]的最小質因子 
            v[i*prime[j]]=prime[j];
        }
    }
}

這便是線性篩。每個合數只會被它的最小質因子篩一次,時間複雜度為O(N)。

篩法介紹完了,求孿生素數對的程式也就不難寫了,只需要判斷與一個素數相差2的數是否為素數即可。這個任務交給讀者自行完成。


下面我簡要說一下測評結果。

這是使用線性篩求對數並輸出孿生素數對的執行結果,總共跑了173.793000s,若僅僅輸出對數,只需要1.410000s,比試除法快了近107倍。

埃氏篩也不敢示弱,跑出了178.203000s和3.105000s的不俗成績。

(順便一提,用試除法求孿生素數對,如果要輸出孿生素數是什麼,它需要跑約328s)

相比於基礎的試除法,這兩種演算法的效率都高得沒話說。

正所謂,永恆與剎那間,只隔著我的演算法。既然有演算法能幾秒內解決問題,那為什麼不用呢?多節約出幾分鐘,不就能多聽幾首銀臨的歌了嗎。

所以,下次有個要求素數的題,嘗試用埃氏篩和線性篩吧。


如果這篇部落格對你有幫助,就請留下一個大拇指,最好還能點個推薦,求求您了,俺求求您了!


Thank you for reading.