矩陣快速冪算法的原理與實踐——“使用MATLAB求解大型斐波那契數"

    無意間翻到了去年寫的公選課MATLAB的課程論文。當時剛好GET了矩陣快速冪,就順便用上了,意料之中地拿了90+分哈哈哈哈(畢竟是公選課)

________________________________________________________________________________________________________________________

①關於MATLAB

   關於MATLAB,有這麼兩點優勢要說明:

    <1>MATLAB擁有着極其方便的矩陣運算,每一個數組都可以表示爲相應維度的矩陣,可以通過兩個數組的直接相乘來獲得“矩陣1×矩陣2”運算的結果矩陣。

    <2>MATLAB擁有着精度超高的符號運算(syms),克服了大數溢出的問題。


   對於諸如C,C++,JAVA等語言,進行高精度數字的求解總是非常具有挑戰性的,即使只是進行大規模整數運算,C++也需要鋪寫大量的代碼定義大數類和重載操作符。如果再加入矩陣運算,使用起來極其不方便。JAVA還好,畢竟大數對它來說不是問題。

   而MATLAB的符號類精度之高非常恐怖,且操作異常方便,可以像處理普通整型一樣地處理符號類。因此,使用MATLAB可以讓“大型”運算變得具有非常強的可行性。

   當然,最好用的果然還是Python了……


②關於“大型斐波那契數”


<1>“斐波那契數”

   斐波那契數列(Fibonacci sequence),又稱黃金分割數列、因數學家列昂納多·斐波那契(LeonardodaFibonacci )以兔子繁殖爲例子而引入,故又稱爲“兔子數列”,指的是這樣一個數列:0、1、1、2、3、5、8、13、21、34、……在數學上,斐波納契數列以如下被以遞推的方法定義:F(0)=0,F(1)=1,F(n)=F(n-1)+F(n-2)(n≥2,n∈N*)在現代物理、準晶體結構、化學等領域,斐波納契數列都有直接的應用。

   而斐波那契數,就是指在已經定義了遞推初始值的前提下,持續遞推斐波那契數列所得的其中一個數。

<2>“大型”

   所謂“大型”,當然不會是求第10個、第20個、第100個斐波那契數這等簡單循環便可以輕易得到結果的數據(當然,其實第100個斐波那契數已經達到了3.54×10­­20的整型存放不下的大數)。

   所謂“大型”,應該是指第一萬個、第十萬、百萬,乃至第千萬、第一億個斐波那契數。根據下圖所示,斐波那契數列有着跟指數函數類似的曲線,到達一定程度後會有類似“指數爆炸”的曲線斜率


③關於“矩陣快速冪”

<1>“矩陣”

   斐波那契數的遞推式可以用一個2×2的矩陣A來表示。


   初始值狀態爲


   根據線性代數知識,容易得以下表達式



   以此類推,第n個斐波那契數的值爲1×2的ans0矩陣的第二個值,在MATLAB中表示的方式爲ans0(2):

<2>“快速冪”

在講述快速冪原理之前上幾張圖:


   矩陣運算相對還是很耗時的

   圖一、圖二、圖三分別計算了求解第103個、第104個、第105個斐波那契數時候所用的時間。分別用了0.393秒,3.517秒以及34.5秒。不難推測,第106個斐波那契數可能需要花費350秒左右的時間。因爲該算法的運算次數隨着橫座標n的變化是線性的。

   而我們要進行對第106、第107、乃至第108個斐波那契數的求解,用這種簡單粗暴的運算方法是行不通的。因此,利用快速冪算法將O(N)的運算優化到O(log2N)來節省運算時間是非常有必要的。


③詳談“快速冪”

   那麼,快速冪究竟是什麼算法呢。

   首先,我們來看看冪運算:

   通常我們都會使用計算機的循環語句進行計算,這樣,當我們計算一個n次方的冪值時,所花費的時間複雜度就是O(N)。因此,如果用每次求下一個斐波那契數的時候,都要乘上一個2×2的矩陣A。用矩陣代替遞推公式求解斐波那契數需要進行n次的矩陣乘法。所花費的時間與上例三張例圖相比好不了多少,甚至可能會花費更長時間。

   這時候就要探討如何減少循環次數進行大規模的冪運算了。

   我們考慮一個問題,當n爲偶數時,設n=2k,我們是否可以把每次循環時候乘上的單元定爲A2呢?這樣可以表示爲

   這樣使用循環計算,就可以節省n/2次的運算,雖然時間複雜度仍然爲O(N),對於大規模運算幾乎沒有多少貢獻,但也爲我們提供了一個思考方向——改變累乘的單位(因爲我講的是矩陣快速冪,因此在之後的文章中,累乘單位稱爲unitMx),以節省循環次數——快速冪的核心想法因此誕生。

   之前我們令unitMx=A與unitMx=A2稱爲靜態地改變,即初始化之後,在循環中就不再變化。這樣做的收效是十分微小的。即使當n%10==0的時候,令unitMx=A10,也無法動搖大規模運算的代價。當然,當n爲平方數的時候,即n=t2的時候,令unitMx=At,可以獲得一個花費爲2*N1/2的算法,時間複雜度O(N1/2),也是相對可觀的。

   但是在快速冪中,unitMX是動態改變的。先看下式:


   是的,將冪次n以“權值×權”的形式表示出來。然而,以10進製爲權進行解釋非常複雜,且沒有實際意義。在快速冪中,我們將冪次n分解成2進制的權。假設n可以化爲p位的二進制數,第i+1位權值設爲bi ,且只有0和1兩個值,即:



   則有:



   每次循環開始的時候就判斷n的二進制表示中,末位是否爲1。如果末位是1的話令ansMx=ansMx*unitMx,否則ansMx=ansMx*1表示不操作。每次循環末尾對n進行向右移位運算。繼續循環。

   這樣,只需要p次循環(p=log2n)就可以完成冪運算了,時間複雜度爲O(log2N)。

④敲代碼前,MATLAB上的幾點問題及改進措施

在熟知原理之後,代碼敲起來也相對較爲順手。

值得一提的是,由於MATLAB命令行窗口輸出結果最多顯示25000位的長度,經過我手動二分計算,算出能夠完整顯示的數字的大小約爲2^108849。只能完整地顯示大約第十萬個斐波那契數。而我們的目標是精確計算序號爲百萬級別、千萬級別、乃至億級別的斐波那契數。

顯然命令行窗口滿足不了我們的需求。

於是,我們嘗試着把輸出結果打印到文件當中。這時候diary函數就派上用場了。diary的主要作用是將matlab工作過程中的全部屏幕文字和數據以文本的方式記錄下來,成爲一個工作日誌讓你查看。然而,我們只需要diary保存結果,而不需要保存命令行日誌。所以,可以在需要打印結果的時候使用diary on,在輸出結束後再用diary off來關閉日誌路徑。輸出函數如下:

function putans(optans)

    diary('C:\Users\60244\Documents\MATLABcodes\ans.txt');

    diary on;

    disp(optans);

    diary off;

end

當然,使用diary無法覆蓋以前的日誌,而我們需要在每次使用程序的時候清空存放結果的文件內容。初始化函數如下:

function initdiary(a,b,m)

    fp=fopen('C:\Users\60244\Documents\MATLABcodes\ans.txt','wt');

    fprintf(fp,'第1個斐波那契數fib[1]=%d\n',eval(a));

    fprintf(fp,'第2個斐波那契數fib[2]=%d\n',eval(b));

    fprintf(fp,'則\nfib[n=%d]=\n',m);

    fclose(fp);

end

還有,由於MATLAB中不含有邏輯移位的操作符,所以,我們必須自己定義邏輯右移的函數:

function shiftR()

    global n;

    if mod(n,2)

        n=(n-1)/2;

    else

        n=n/2;

    end

end

⑤貼代碼



⑥貼結果

先直接來看運行結果,同最初簡單粗暴的算法相比,新的算法果然節省了大量的運算時間。


圖1:求解第105個斐波那契數


圖2:求解第106個斐波那契數


圖3:求解第107個斐波那契數


圖4:求解第108個斐波那契數

 

註釋掉代碼第32行的輸出指令之後,再次進行求解第10^8個斐波那契數的運算,會發現運行時間減少了一半多(如下圖)。

                                         

         尋找原因:

                         

打開目標目錄看輸出文件ans.txt的屬性(圖),僅僅存儲一個斐波那契數,就佔用了整整19.9MB的磁盤空間,換算後,約有2*10^7位十進制數(通過代碼對文件中的fib[100000000]進行精確統計後,第10^8個斐波那契數總共有20898767位)

                                        

可見,當數字規模到達一定程度後,數字的打印速度會桎梏該程序代碼的運算速度。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章