1. 程式人生 > >有關素數判斷的一些算法(總結&&對比)

有關素數判斷的一些算法(總結&&對比)

stat names 最小 csdn fread AR 目前 ike new

素性測試是數論題中比較常用的一個技巧。它可以很基礎,也可以很高級(哲學)。這次主要要介紹一下有關素數判斷的奇技淫巧

素數的判斷主要分為兩種:範圍篩選型&&單個判斷型

我們先從範圍篩選型這種常用的開始講起,這裏采用模板題Luogu P3383 【模板】線性篩素數來進行測試

1.埃氏篩

這是最常用的篩法了,思路也很簡單:任何一個素數的倍數都是合數

然後我們O(n)掃一遍,同時篩去素數的倍數

但是有一些數如6,會被2和3都篩去一次,就造成了效率上的浪費,所以復雜度經證明為**O(n log log n)

CODE

#include<cstdio>
using namespace std;
const int N=10000005;
bool vis[N];
int n,m,x;
inline char tc(void)
{
    static char fl[100000],*A=fl,*B=fl;
    return A==B&&(B=(A=fl)+fread(fl,1,100000,stdin),A==B)?EOF:*A++;
}
inline void read(int &x)
{
    x=0; char ch=tc();
    while (ch<‘0‘||ch>‘9‘) ch=tc();
    while (ch>=‘0‘&&ch<=‘9‘) x=x*10+ch-‘0‘,ch=tc();
}
inline void get_prime(int m)
{
    register int i,j;
    for (vis[1]=1,i=2;i<=m;++i)
    if (!vis[i]) for (j=i<<1;j<=m;j+=i) vis[j]=1;
}
int main()
{
    //freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
    read(n); read(m); get_prime(n);
    while (m--)
    {
        read(x); 
        puts(vis[x]?"No":"Yes");
    }
    return 0;
}

2.線性篩(歐拉篩)

這其實是對上者的優化,我們意識到一個數應該只有它的最小質因數刪去,所以我們可以一邊篩數的同時一邊記錄素數,這就是真正的O(n)復雜度

CODE

#include<cstdio>
using namespace std;
const int N=10000005;
int prime[N],n,m,x,cnt;
bool vis[N];
inline char tc(void)
{
    static char fl[100000],*A=fl,*B=fl;
    return A==B&&(B=(A=fl)+fread(fl,1,100000,stdin),A==B)?EOF:*A++;
}
inline void read(int &x)
{
    x=0; char ch=tc();
    while (ch<‘0‘||ch>‘9‘) ch=tc();
    while (ch>=‘0‘&&ch<=‘9‘) x=x*10+ch-‘0‘,ch=tc();
}
inline void Euler(int n)
{
    register int i,j;
    for (vis[1]=1,i=2;i<=n;++i)
    {
        if (!vis[i]) prime[++cnt]=i;
        for (j=1;j<=cnt&&i*prime[j]<=n;++j)
        {
            vis[i*prime[j]]=1;
            if (!(i%prime[j])) break;
        }
    }
}
int main()
{
    //freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
    read(n); read(m);
    Euler(n);
    while (m--)
    {
        read(x);
        puts(vis[x]?"No":"Yes");
    }
    return 0;
}

註意上面的那句話:

if (!(i%prime[j])) break;

這保證了線性篩的效率,不會產生重復,因為當i%prime[j]==0時這個數就是讓後面的數刪去。

3.基礎素性測試

這是最基本的素數判定法了吧。從2到sqrt(x)枚舉是否有數能夠整除x

證明的話很簡單,因為如果這個數是素數,那麽它的因數必定為1和x,若其因數大於sqrt(x),那麽平方後就大於x,這顯然不可能。

所以我們O(sqrt(x))判斷一次

CODE

#include<cstdio>
#include<cmath>
using namespace std;
int n,m,x;
inline char tc(void)
{
    static char fl[100000],*A=fl,*B=fl;
    return A==B&&(B=(A=fl)+fread(fl,1,100000,stdin),A==B)?EOF:*A++;
}
inline void read(int &x)
{
    x=0; char ch=tc();
    while (ch<‘0‘||ch>‘9‘) ch=tc();
    while (ch>=‘0‘&&ch<=‘9‘) x=x*10+ch-‘0‘,ch=tc();
}
inline bool check(int x)
{
    if (!(x^1)) return 0;
    register int i; int bound=(int)sqrt(x);
    for (i=2;i<=bound;++i)
    if (!(x%i)) return 0;
    return 1;
}
int main()
{
    //freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
    read(n); read(m);
    while (m--)
    {
        read(x);
        puts(check(x)?"Yes":"No");
    }
    return 0;
}

4.對於算法3的優化

首先我們看一個結論:

大於等於5的質數一定和6的倍數相鄰。

證明等參考:dalao‘s blog

然後同3,我們只不過每次快進6個單位,然後常數就得到了難以言喻都優化(一躍成為此題最快的算法)

CODE

#include<cstdio>
#include<cmath>
using namespace std;
int n,m,x;
inline char tc(void)
{
    static char fl[100000],*A=fl,*B=fl;
    return A==B&&(B=(A=fl)+fread(fl,1,100000,stdin),A==B)?EOF:*A++;
}
inline void read(int &x)
{
    x=0; char ch=tc();
    while (ch<‘0‘||ch>‘9‘) ch=tc();
    while (ch>=‘0‘&&ch<=‘9‘) x=x*10+ch-‘0‘,ch=tc();
}
inline bool check(int x)
{
    if (!(x^1)) return 0;
    if (!(x^2)||!(x^3)) return 1;
    if ((x%6)^1&&(x%6)^5) return 0;
    register int i; int bound=(int)sqrt(x);
    for (i=5;i<=bound;i+=6)
    if (!(x%i)||!(x%(i+2))) return 0;
    return 1;
}
int main()
{
    //freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
    read(n); read(m);
    while (m--)
    {
        read(x);
        puts(check(x)?"Yes":"No");
    }
    return 0;
}

5.Miller-Rabin算法

這是歷史上判斷素數最快的方法了吧(但在此題中被算法4吊打了)

首先,這個算法基於費馬小定理和二次探測定理:

二次探測定理:如果p是奇素數,則 x2≡1(modp)的解為x = 1或x = p - 1(mod p)

所以我們可以把x變成r*2^t的形式,其中r是一個奇數

然後我們結合兩種算法&&快速冪就可以穩定O(log x)進行單次判斷了

但是這個算法是一個非完美算法,它每一次都25%的概率是錯的,所以我們可以多選擇幾個數都弄幾次

但是偶然在網上看到一段話:

對於大數的素性判斷,目前Miller-Rabin算法應用最廣泛。一般底數仍然是隨機選取,但當待測數不太大時,選擇測試底數就有一些技巧了。比如,如果被測數小於4 759 123 141,那麽只需要測試三個底數2, 7和61就足夠了。當然,你測試的越多,正確的範圍肯定也越大。如果你每次都用前7個素數(2, 3, 5, 7, 11, 13和17)進行測試,所有不超過341 550 071 728 320的數都是正確的。如果選用2, 3, 7, 61和24251作為底數,那麽10^16內唯一的強偽素數為46 856 248 255 981。這樣的一些結論使得Miller-Rabin算法在OI中非常實用。通常認為,Miller-Rabin素性測試的正確率可以令人接受,隨機選取k個底數進行測試算法的失誤率大概為4^(-k)。

所以對於這一題n=10000000的範圍就只需要選擇2,7,61即可

CODE

#include<cstdio>
using namespace std;
typedef long long LL;
const int prime[3]={2,7,61};
int n,m,x;
inline char tc(void)
{
    static char fl[100000],*A=fl,*B=fl;
    return A==B&&(B=(A=fl)+fread(fl,1,100000,stdin),A==B)?EOF:*A++;
}
inline void read(int &x)
{
    x=0; char ch=tc();
    while (ch<‘0‘||ch>‘9‘) ch=tc();
    while (ch>=‘0‘&&ch<=‘9‘) x=x*10+ch-‘0‘,ch=tc();
}
inline int quick_pow(int x,int p,int mod)
{
    int tot=1;
    while (p)
    {
        if (p&1) tot=((LL)tot*x)%mod;
        x=((LL)x*x)%mod; p>>=1;
    }
    return tot;
}
inline bool Miller_Rabin(int x)
{
    if (!(x^2)) return 1;
    if (x<2||!(x&1)) return 0;
    int t=0,u=x-1;
    while (!(u&1)) ++t,u>>=1;
    for (register int i=0;i<3;++i)
    {
        if (!(x^prime[i])) return 1;
        if (!(x%prime[i])) return 0;
        int lst=quick_pow(prime[i],u,x);
        for (register int j=1;j<=t;++j)
        {
            int now=((LL)lst*lst)%x;
            if (!(now^1)&&lst^1&&lst^(x-1)) return 0; lst=now;
        }
        if (lst^1) return 0;
    }
    return 1;
}
int main()
{
    //freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
    read(n); read(m);
    while (m--)
    {
        read(x);
        puts(Miller_Rabin(x)?"Yes":"No");
    }
    return 0;
}

最後給出5個算法的運行結果(無O2)

  1. 埃氏篩

  2. 線性篩(歐拉篩)

  3. 基礎素性測試

  4. 優化的算法3

  5. Miller-Rabin

有關素數判斷的一些算法(總結&&對比)