Haskell: 基於方陣快速冪,求解斐波那契數列項

Haskell: 基於方陣快速冪,求解斐波那契數列項

原理

  • 斐波那契:F(n+1)=F(n)+F(n1)F(n+1) = F(n) + F(n-1), F(1)=0,F(0)=1,F(1)=1,F(2)=2,...F(-1) = 0, F(0) = 1, F(1) = 1, F(2) = 2, ...

  • 使用矩陣的冪求解斐波那契:
    [F(n+1)F(n)]=[1110][F(n)F(n1)] \left[ \begin{array}{c} F(n+1) \\ F(n) \end{array} \right]=\left[ \begin{array}{cc} 1 & 1\\ 1 & 0 \end{array} \right]*\left[ \begin{array}{c} F(n) \\ F(n-1) \end{array} \right]
    [F(n+1)F(n)]=[1110]n[F(1)F(0)]=[1110]n[11] \left[ \begin{array}{c} F(n+1) \\ F(n) \end{array} \right]=\left[ \begin{array}{cc} 1 & 1\\ 1 & 0 \end{array} \right]^n*\left[ \begin{array}{c} F(1) \\ F(0) \end{array} \right]=\left[ \begin{array}{cc} 1 & 1\\ 1 & 0 \end{array} \right]^n*\left[ \begin{array}{c} 1 \\ 1 \end{array} \right]

  • 矩陣的快速冪方法:基於公式A2m=AmAmA^{2m}=A^m*A^m和分治法,可以通過O(logn)O(logn)的對數時間計算AnA^{n}

  • 可以通過快速冪來快速求解 [1110]n\left[ \begin{array}{cc} 1 & 1\\ 1 & 0 \end{array} \right]^n,從而在對數時間內求解斐波那契的第n項。

代碼實現

實現方陣的乘法

my_product :: Num a => [[a]] -> [[a]] -> [[a]]
my_product a b = [[sum [a !! i !! k * b !! k !! j | k <- [0..dim - 1]] | j <- [0..dim - 1]] | i <- [0..dim - 1]]
                 where dim = 2

定義中dim=2是方陣的維數。因爲要用到的是2*2方陣的乘法,所以dim=2.

然後,因爲此處是自乘,寫一個自乘函數比較方便:

self_product :: Num a => [[a]] -> [[a]]
self_product = \a -> my_product a a

這樣做,比直接定義自乘函數泛用性要強,而且並不會使計算變慢。

Haskell有惰性求值的特性,在上述函數中,參數a被函數體中的my_product第一次引用時求值,但是隻會求一次值,第二次引用時不會重新求一遍。

實現分治法快速求冪

以下是第一版代碼:

fast_exp :: (Num a, Integral int) => [[a]] -> int -> [[a]]
fast_exp _ 0 = [[1, 0], [0, 1]]
fast_exp a num = if num `mod` 2 == 0 
                 then self_product $ fast_exp a $ halfnum
                 else my_product a $ self_product $ fast_exp a $ halfnum
                 where halfnum = floor $ fromIntegral num / 2
  • 腦子裏時刻想明白求值順序,通過檢查現在寫出來的
  • 這裏主要練習了一下$的使用。$的使用其實就是改變求值順序。
    • 編程時,應該先想明白求值順序,然後靈活使用$,而不是用$等價替換括號。
    • 編程時,永遠不要想着用$完全代替括號,但是在括號嵌套很深的時候,可以通過使用$減少括號的數量。
  • 注意halfnum的實現。爲了類型正確,中間需要一步fromIntegral
  • 以上提到的所有函數,如果用到,一定通過:help來查看函數的具體函數簽名,來保證寫出來的程序正確。

在此基礎上,試着不用if-else,而是使用模式匹配寫出來:

fast_exp :: (Num a, Integral int) => [[a]] -> int -> [[a]]
fast_exp a num | num == 0 = [[1, 0], [0, 1]]
               | num `mod` 2 == 0 = self_product $ fast_exp a $ halfnum
               | otherwise = my_product a $ self_product $ fast_exp a $ halfnum
               where halfnum = floor $ fromIntegral num / 2

這樣寫更有Haskell風範。

實現斐波那契數列項的求解

fibo :: Integral int => int -> int
fibo num = fast_exp fibo_base num !! 0 !! 0 where fibo_base = [[1, 1], [1, 0]]

通過快速冪求解矩陣,然後再取出對應位置的元素,即爲數列的項值。

完成

在終端裏面運行ghci打開Haskell交互平臺,用:l加載腳本,然後可以調用函數求解。

*Main> fibo 100
573147844013817084101
(0.00 secs, 159,264 bytes)

測試時間

:set +s顯示函數執行狀態,從而可以測試指令執行時間。
在此基礎上,用下面這個函數封裝一下,使得計算過程不變,但是不會被顯示數字的時間干擾。

my_zero :: Integral int => int -> int
my_zero a = if a == 0 then 0 else 1

通過下面的執行結果可見,顯示一長串數字確實影響對測試結果的計算。用my_zero封裝一下可以排除顯示數字所需時間的干擾。

*Main> fibo 2000
6835...26(many numbers)
(0.05 secs, 569,920 bytes)

*Main> my_zero $ fibo 2000
1
(0.02 secs, 208,080 bytes)

*Main> my_zero $ fibo 200000
1
(0.00 secs, 509,608 bytes)

那麼我們測試一下速度(從1e7開始倍增):

*Main> my_zero $ fibo 10000000
1
(0.16 secs, 12,081,960 bytes)
*Main> my_zero $ fibo 20000000
1
(0.33 secs, 23,806,616 bytes)
*Main> my_zero $ fibo 40000000
1
(0.73 secs, 47,246,816 bytes)
*Main> my_zero $ fibo 80000000
1
(1.61 secs, 94,117,912 bytes)
*Main> my_zero $ fibo 160000000
1
(3.53 secs, 187,849,912 bytes)

果然是一個對數時間複雜度。
(在數字達到1e9之後,可能因爲內存不夠用了而時間上漲。但整體沒什麼問題。)

完整代碼

-- 方陣快速冪.hs

my_product :: Num a => [[a]] -> [[a]] -> [[a]]
my_product a b = [[sum [a !! i !! k * b !! k !! j | k <- [0..dim - 1]] | j <- [0..dim - 1]] | i <- [0..dim - 1]]
                 where dim = 2

{-
self_product :: Num a => [[a]] -> [[a]]
self_product a = [[sum [a !! i !! k * a !! k !! j | k <- [0..1]] | j <- [0..1]] | i <- [0..1]]
-}

self_product :: Num a => [[a]] -> [[a]]
self_product = \a -> my_product a a

{-
fast_exp :: (Num a, Integral int) => [[a]] -> int -> [[a]]
fast_exp _ 0 = [[1, 0], [0, 1]]
fast_exp a num = if num `mod` 2 == 0 
                 then self_product $ fast_exp a $ halfnum
                 else my_product a $ self_product $ fast_exp a $ halfnum
                 where halfnum = floor $ fromIntegral num / 2
                 -}

fast_exp :: (Num a, Integral int) => [[a]] -> int -> [[a]]
fast_exp a num | num == 0 = [[1, 0], [0, 1]]
               | num `mod` 2 == 0 = self_product $ fast_exp a $ halfnum
               | otherwise = my_product a $ self_product $ fast_exp a $ halfnum
               where halfnum = floor $ fromIntegral num / 2

fibo :: Integral int => int -> int
fibo num = fast_exp fibo_base num !! 0 !! 0 where fibo_base = [[1, 1], [1, 0]]

my_zero :: Integral int => int -> int
my_zero a = if a == 0 then 0 else 1

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