無意間翻到了去年寫的公選課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×1020的整型存放不下的大數)。
所謂“大型”,應該是指第一萬個、第十萬、百萬,乃至第千萬、第一億個斐波那契數。根據下圖所示,斐波那契數列有着跟指數函數類似的曲線,到達一定程度後會有類似“指數爆炸”的曲線斜率。
③關於“矩陣快速冪”
<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位)
可見,當數字規模到達一定程度後,數字的打印速度會桎梏該程序代碼的運算速度。