Miller-Rabin概率素數測試算法

本文首先鳴謝以下資料文章:
資料1
資料2
資料3
下面我們開始正文,從源頭開始真正的梳理一下素數測試

1.素數

我們都知道,素數在當今的數論中佔有非常重要的地位,主要原因就是素數最根本的性質——除了1,和自身以外,不會被任何一個數整除
並且,素數現在在我們的日常生活中伴有非常重要的地位,這一點的其一主要原因就是素數已經是密碼學中最重要的一點,我們當今的密碼學常常要涉及到利用超大素數作爲我們的密鑰和核心,所以說,我們對素數的研究就變得非常的重要了
很是遺憾,現在我們並沒有一套合理的算法體系去真正的獲取一個絕對100%是素數的一個非常大的數,這是當前做不到的,因爲目前的確定性的算法朝朝大素數無外乎兩種情況,耗時和空間佔用,但是本文將從一種全新的角度帶大家研究一種非確定性算法,該算法雖然並不是100%正確的,但是我們如果加上限制條件,我們可以將該算法的錯誤率降低到幾乎可以忽略的程度,這也就是我們本文即將講述的重點——Miller-Rabin算法

2.樸素素數測試 + 篩法

我們從剛開始學編程的時候,大學老師都會給我們將一種非常糟糕的算法(當然這處理我們的考試已經夠了)這種算法叫做試除法
算法描述:
對一個已知的待測的數n,k從2開始一直到n-1,我們如果發現k|n,那麼我們認爲這是一個合數
當然,這是我們從素數的定義出發的一種算法,之所以說這種算法糟糕是因爲當我們的待測的數非常的大的時候,我們不得不遍歷一整遍數據來保證算法的正確性,這是無法容忍的
可能有的人還想在這個算法上搞搞優化什麼的,其實都是治標不治本
至於樸素素數測試和篩法測試我會援引我的博客作爲講解
Lantian的樸素素數測試和篩法素數生成算法講解
在這裏我先聲明:
篩法是一種非常高效的算法,但是在這裏篩法沒有辦法發揮他的優勢,因爲篩法真正強大在可以快速的生成一定範圍內的所有的素數,但是我們這裏強調的是對超大素數的測試,並不需要獲取那麼多的素數

以上,篩法和樸素測試的時間複雜度都是O(n)和無限接近O(n),在這裏我們確定性算法就走到了盡頭,下面有請非確定性概率測試算法來施展身手

3.必要數論基礎知識

在我們繼續研究之前,我們還需要一些必備的知識來爲我們打通道路
1.費馬小定理:
其實在講費馬小定理之前,我們其實還需要講解歐拉定理,飛馬小丁立只是歐拉定理的特殊情況
歐拉定理:
歐拉定理這裏的n,a必須是互素(Gcd(n,a)=1)
費馬小定理:
費馬小定理當歐拉定理中的n是素數的時候,很顯然歐拉函數的值是n-1,費馬小定理成立,這裏就不描述費馬小定理的證明了
2.二次探測定理:
二次探測定理
爲了更好的瞭解Miller-Rabin算法,我們在這裏必須需要了解二次探測定理的證明和原理,相信我,這不難
首先,先給出二次探測定理的描述:
如果p是素數,x是小於p的正整數,且,那麼要麼x=1,要麼x=p-1。這是顯然的,因爲相當於p能整除,也即p能整除(x+1)(x-1)。由於p是素數,那麼只可能是x-1能被p整除(此時x=1) 或 x+1能被p整除(此時x=p-1)。

4.費馬測試

首先我們先來看看費馬測試
剛纔我們的費馬小定理已經說明了素數成立的必要條件,也就是說,如果一個數不滿足費馬小定理,那麼這個數必定是合數,但是如果這個數滿足我們就沒有辦法確定是不是合數還是素數了,因爲歷史上有一種非常神祕的數的存在——卡密歇爾數,這類數我們也叫僞素數
如果想要了解更多的話,可以百度查詢,我們這裏只需要瞭解到,因爲卡米歇爾書滿足費馬小定理但是同時又不是素數,所以這使得我們的費馬測試(費馬小定理的逆定理)不是正確的,也就不能稱之爲算法
但是我們還是需要知道這種測試的情況的,對於至少也是一種測試算法
一般以2爲a做測試,我們一般應用費馬測試的時候都是提前利用了一張僞素數表來進行容錯處理,當我們找到了滿足費馬測試並且又不在僞素數表(基於底數2)上的時候我們就可以斷定是一個素數,但是這樣有兩個缺點:
1.佔用時間,我們生成僞素數需要很大的計算資源(我還真不知道有什麼好的算法可以快速求僞素數)
2.當我們內存資源不允許僞素數表的時候,我們的費馬測試錯誤率太高,不能實際應用

費馬小定理畢竟只是素數判定的一個必要條件.滿足費馬小定理條件的整數n未必全是素數.有些合數也滿足費馬小定理的條件*.這些合數被稱作Carmichael數,前3個Carmichael數是561,1105,1729.
Carmichael數是非常少的.在1~100000000範圍內的整數中,只有255個Carmichael數.數據越大,之後的卡米歇爾數越稀疏
但是在允許僞素數表的情況下對於快速計算冪取模的話(因爲測試的素數非常的大)我們可以用快速冪來實現:Lantian的快速冪算法詳解

5.Miller-Rabin素性測試

Miller和Rabin兩個人的工作讓Fermat素性測試邁出了革命性的一步,建立了Miller-Rabin素性測試算法
在這之前,我們爲了更好的瞭解算法的本質,我們來看一下僞素數341是如何被Miller-Rabin的二次探測定理卡掉的
一下摘引自資料1:
我們下面來演示一下上面的定理如何應用在Fermat素性測試上。前面說過341可以通過以2爲底的Fermat測試,因爲2^340 mod 341=1。如果341真是素數(對於任意的x<341,我們必須都要滿足x=1||x=340)的話,那麼2^170(2^340開方,這時候的2^340滿足了)mod 341只可能是1或340;當算得2^170 mod 341確實等於1時,我們可以繼續查看2^85除以341的結果。我們發現,2^85 mod 341=32,這一結果摘掉了341頭上的素數皇冠

在這裏,我們抽離一下本質,我們用Miller-Rabin做素數測試的時候將a^(n-1)
轉化成了a^(d*2^r)這裏的d是一個正奇數(1也是)

這就是Miller-Rabin素性測試的方法。不斷地提取指數n-1中的因子2,把n-1表示成(其中d是一個奇數)。那麼我們需要計算的東西就變成了除以n的餘數。於是,要麼等於1,要麼等於n-1。如果等於1,定理繼續適用於,這樣不斷開方開下去,直到對於某個i滿足或者最後指數中的2用完了得到的
在這裏我們需要明確一點,當這種情況出現的時候,我們沒有辦法繼續滿足二次探測定理了,我們就不對這種情況繼續判斷,支隊等於1的情況繼續用二次探測定理判斷

所以我們的算法流程就出來了
我們首先從先計算出
x=(mod n)
然後如果x=n-1,我們返回true,是一個素數
如果不是我們繼續判斷知道,我們中途發現x!=1&&x!=n-1我們返回false,是個合數
知道最後,我們看看剩下的數是1還是n-1還是別的數

在這裏我們還有一些技巧需要學習:
1.利用數論的只是證明之後我們可以發現,只要我們的Miller-Rabin多次隨機選擇底數a的話,重複進行k次,我們可以將錯誤降低到2^(-k),次數越多越精確,錯誤概率越小
2.Miller-Rabin素性測試同樣是不確定算法,我們把可以通過以a爲底的Miller-Rabin測試的合數稱作以a爲底的強僞素數(strong pseudoprime)。第一個以2爲底的強僞素數爲2047。第一個以2和3爲底的強僞素數則大到1 373 653。
Miller-Rabin算法的代碼也非常簡單:計算d和r的值(可以用位運算加速,即快速積,快速冪),然後二分計算的值,最後把它平方r次。
3.對於大數的素性判斷,目前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內唯一的強僞素數爲46856248255981
4.最好不要用合數作爲底,出錯概率太大,至少也是素數作爲底,證明的話,不會

6.Miller-Rabin 代碼

本人用正向迭代和反向迭代都了一遍,發現正向迭代在很大的偶數的情況下比反向的速度快一點點,平均的時間都差不多

#include"iostream"
#include"cstdio"
#include"cstring"
#include"cstdlib"
#include"ctime"
using namespace std;

long long quicks(long long a,long long b,long long c)
{
    long long ans=1;
    a=a%c;
    while(b!=0)
    {
        if(b & 1) ans=(ans*a)%c;
        b>>=1;
        a=(a*a)%c;
    }
    return ans;
}

bool Miller_Rabin_1(long long n)   //標準代碼 
{
    long long t=0;
    long long b=n-1;
    while((b&1)==0)
    {
        t++;
        b>>=1;
    }
    //現在的a^(b*2^t)=1(mod n)
    long long a=11;   //測試
    long long x=quicks(a,b,n);
    //個人認爲這裏如果加上優先判定是不是1,n-1的話,會更快一點?是不是呢????? 
    for(long long i=1;i<=t;i++)
    {
        long long y=quicks(x,2,n);
        if(y==1&&x!=1&&x!=n-1) return false;    //這裏的意思是如果a^(d*2^r)是1,但是a^(d*2^(r-1))不是1也不是n-1的情況,這時候我們認爲是合數 
        x=y;
    } 
    if(x!=1) return false;
    else return true;
}

bool Miller_Rabin_2(long long n)   //正向迭代 
{
    long long p=n-1;
    long long a=11;
    long long x=quicks(a,p,n);
    if(x==n-1) return true;
    else
    {
        long long w;
        do
        {
            p>>=1;
            w=quicks(a,p,n);
            if(w==n-1) return true;
            else if(w!=1) return false;
        }
        while((p&1)!=1);

        if(w==1||w==n-1) return true;
        else return false;
    }
}


int main()
{
    double time=clock();
    if(Miller_Rabin_1(2222222222222222222222)) printf("YES\n");
    else printf("NO\n");
    printf("%lf\n",clock()-time);
    time=clock();
    if(Miller_Rabin_2(2222222222222222222222)) printf("YES\n");
    else printf("NO\n");
    printf("%lf\n",clock()-time);
    return 0;
}

7.Thinking

感謝前人的無私分享,我在匍匐前進,特以此文鳴謝資料和論文的作者
剩餘幾個問題待解:
1.算法中最後的(n-1)|x的情況爲什麼不考慮,只考慮x!=1的情況?
2.在檢查的過程中,如果出現了(n-1)|x我們怎麼迴避呢
3.在計算出來d,b之後我們可不可以提前判斷一下a^d是不是1或者n-1的倍數從而加快速度呢?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章