1. 程式人生 > 實用技巧 >【數論】多種素數判斷法及素數篩法

【數論】多種素數判斷法及素數篩法


素數判斷法

樸素判斷

眾所周知,大於等於\(2\)的僅含有\(1\)和自身這兩個因子的正整數被稱作素數

故只要判斷在\([2,n-1]\)範圍內是否存在其它因子,就可以判斷\(n\)是否為素數了

無腦判斷法直接從\(2\)for到\(n-1\),稍微優化下可以縮減到\([2,\frac n 2]\),但這個方法時間複雜度為\(O(n)\)

可以發現,如果某個數\(x\)\(n\)的因子,那麼\(\frac n x\)也一定是\(n\)的因子(可能\(x==\frac n x\)

所以如果將這兩個因子看作一組,我們其實只需要判斷\([2,\sqrt n]\)以內是否有素數即可,時間複雜度為\(O(\sqrt n)\)

#include<cmath>

bool isprime(int n)
{
    int d=sqrt(n);
    for(int i=2;i<=d;i++)
        if(n%i==0)
            return false;
    return true;
}
bool isprime2(int n) //寫著比較方便,下面都用這種寫法
{
    for(int i=2;i*i<=n;i++)
        if(n%i==0)
            return false;
    return true;
}


六除法判斷

根據素數(\(≥5\)

)的分佈性質可以得知

每個素數都與\(6\)的倍數相鄰

例如\(5,7\ ;\ 11,13\ ;\ 17,19...\)

所以得出的結論是,如果一個\(≥5\)的數對\(6\)取模不等於\(1\)或者\(5\),那麼這個數一定不是素數

於是我們就能從\(5\)開始,以\(6\)為步數判斷到\(\sqrt n\)過,每次只需要判斷\(i\)\(i+2\)是否為因子,大大提升了判斷效率

時間複雜度小於\(O(\sqrt n)\)

bool isprime(int n)
{
    if(n<=3) //特判2,3
        return n>1;
    if(n%6!=1&&n%6!=5)
        return false;
    for(int i=5;i*i<=n;i+=6)
        if(n%i==0||n%(i+2)==0)
            return false;
    return true;
}


篩法判斷素數

下文將會闡述幾種篩法

通過\(O(n)\)\(O(nloglogn)\)幾種不同時間複雜度篩出素數後實現\(O(1)\)查詢

但將會受限於空間



Miller-Rabin素數檢測

這是對於一個不能在\(O(\sqrt n)\)時間範圍內判斷出是否為素數的大數的測試方法

通過選取小素數去測試被測數是否為素數

但非素數也有\(\frac 1 4\)的概率會通過檢測

所以需要多次選取不同素數進行測試,若都通過則大概率為素數

注:如果\(int\)範圍內的某個被測數用\(30\)以內的素數均能通過測試,可以得到該數為素數。

引理 1 ——費馬小定律:

​ 設\(p\)是素數,\(a\)為整數,且\((a,p)=1\),則\(a^{p-1}≡1\ (mod\ p)\)

引理 2 ——二次探測定理:

​ 如果\(p\)是一個素數,且\(0<x<p\),則方程\(x^2≡1\ (mod\ p)\)的解為\(x_1=1,x_2=p-1\)

所以該演算法,流程如下

  1. \(s,t\)使得\(2^s*t=x-1\)\(t\%2==1\)

  2. 取一小素數\(a\),算出\(a^t\),然後不斷平方並進行二次檢測(進行\(s\)次)

  3. 根據費馬小定律,如果\(a^{x-1}\ !≡\ 1\ (mod\ p)\),則可得\(x\)非素數

  4. 多次進行檢測,可使得正確性概率更高

typedef long long ll;

int prim[20]={2,3,5,7,11,13,17,19,23,29};

ll qmul(ll a,ll b,ll mod) //快速乘
{
    ll r=0;
    while(b)
    {
        if(b&1)
            r=(r+a)%mod;
        a=(a+a)%mod;
        b>>=1;
    }
    return r;
}
ll qpow(ll a,ll n,ll mod) //快速冪
{
    ll r=1;
    while(n)
    {
        if(n&1)
            r=(r*a)%mod;
        n>>=1;
        a=(a*a)%mod;
    }
    return r;
}

bool Miller_Rabin(ll x)
{
    int s=0;
    ll t=x-1;
    if(x<=3)
        return x>1;
    if(!(x&1))
        return false; //特判一些基本情況
    while(!(t&1)) //將x-1分解成(2^s)*t
    {
        s++;
        t>>=1;
    }
    for(int i=0;i<10&&prim[i]<x;i++)
    {
        ll b=qpow(prim[i],t,x);
        for(int j=1;j<=s;j++) //s次平方
        {
            ll k=qmul(b,b,x);
            if(k==1&&b!=1&&b!=x-1) //用二次探測判斷
                return false;
            b=k;
        }
        if(b!=1)
            return false; //用費馬小定律判斷
    }
    return true;
}



素數篩法

樸素篩法

基於樸素判斷法的樸素篩法

複雜度\(O(n^{\frac 3 2})\),很慢

bool prime[maxn];
bool isprime(int n)
{
    for(int i=2;i*i<=n;i++)
        if(n%i==0)
            return false;
    return true;
}
void primeSelect()
{
    prime[0]=prime[1]=false;
    for(int i=2;i<=maxn;i++)
        prime[i]=isprime(i);
}


Eratosthenes篩法/埃氏篩

埃氏篩法是一種從\(2\)開始,通過質因子列舉的方式篩去所有合數,使得最後留下的均為素數

如果數字\(x\)是素數,那麼所有以\(x\)為因子的數均非素數(素數性質)

所以接下來可以列舉以\(x\)為因子的數並標記為合數即可

優化1:每次找到一個素數\(x\),列舉下界可以是\(x^2\)而非\(2x\)(可證得),可小幅度縮短篩選時間

優化2:(根據優化1可得)素數列舉只要求到\(\sqrt n\)即可篩出\(1\)\(n\)內的所有素數

時間複雜度\(O(nloglogn)\),較為高效(還好寫)

bool prime[maxn];
void primeSelect(int n)
{
    memset(prim,true,sizeof prim);
    prim[0]=prim[1]=false;
    for(int i=2;i*i<=n;i++) //篩到sqrt n即可
        if(prim[i])
            for(int j=i*i;j<=n;j+=i) //以i*i為下界
                prim[j]=false;
}

如需素數記錄,外層迴圈改為\(2\)\(n\)即可

int prime[maxn],cnt=0;
bool vis[maxn];
void primeSelect(int n)
{
    for(int i=2;i<=n;i++)
        if(!vis[i])
        {
            prime[++cnt]=i;
            for(int j=i*i;j<=n;j+=i)
                vis[j]=false;
        }
}


尤拉篩

可以發現,埃氏篩法雖然較為高效,但未經判斷直接開始列舉質因子的倍數則會產生一定數量的重複標記

重複對某些已經篩出的合數進行標記,只會浪費複雜度,這也正是其\(O(nloglogn)\)的由來(在一定程度上,\(loglogn\)已經非常高效)

但我們不滿足於這種看似高效的演算法,而需要真正意義上的線性篩

所以在埃氏篩法的基礎上,讓每個合數只被它的最小質因子篩選到一次即判斷退出,以達到不重複的目的

這便是尤拉篩法,時間複雜度\(O(n)\)

int prime[maxn],cnt=0;
bool vis[maxn];
void primeSelect()
{
    memset(vis,false,sizeof vis);
    for(int i=2;i<=maxn;i++)
    {
        if(!vis[i]) //沒訪問過的就是素數
            prime[++cnt]=i;
        for(int j=1;j<=cnt&&i*prime[j]<=maxn;j++) //遍歷每一個已找到的素數
        {
            vis[i*prime[j]]=true;
            if (i%prime[j]==0) //每個數只被它的最小質因子篩一次
                break;
        }
    }
}



參考文章:

《Miller-Rabin素數測試演算法》 - forever_dreams