有關素數判斷的一些算法(總結&&對比)
素性測試是數論題中比較常用的一個技巧。它可以很基礎,也可以很高級(哲學)。這次主要要介紹一下有關素數判斷的奇技淫巧
素數的判斷主要分為兩種:範圍篩選型&&單個判斷型
我們先從範圍篩選型這種常用的開始講起,這裏采用模板題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)
埃氏篩
線性篩(歐拉篩)
基礎素性測試
優化的算法3
Miller-Rabin
有關素數判斷的一些算法(總結&&對比)