2013-06-26 40 views
2

我的一位朋友向我展示了他參加的C++課程的家庭練習。由於我已經知道C++,但剛開始學習Haskell時,我試圖用「Haskell方式」來解決這個問題。更高效的算法使Haskell變得更糟

這些都是運動指令(我從我們的母語翻譯,所以請評論,如果指示不明確):

寫一個程序讀取非零係數(A,B,C,d)並將它們置於下列公式中: A * x + B * y + C * z = D 程序還應該從用戶N讀取,它表示一個範圍。程序應該找到範圍在-N/2到N/2範圍內的所有可能的整體解。

例如:

Input: A = 2,B = -3,C = -1, D = 5, N = 4 
Output: (-1,-2,-1), (0,-2, 1), (0,-1,-2), (1,-1, 0), (2,-1,2), (2,0, -1) 

最直接的算法是試圖通過暴力破解所有的可能性。我實現了它在Haskell以下列方式:

triSolve :: Integer -> Integer -> Integer -> Integer -> Integer -> [(Integer,Integer,Integer)] 
triSolve a b c d n = 
    let equation x y z = (a * x + b * y + c * z) == d 
     minN = div (-n) 2 
     maxN = div n 2 
    in [(x,y,z) | x <- [minN..maxN], y <- [minN..maxN], z <- [minN..maxN], equation x y z] 

到目前爲止好,但運動指令注意,更高效的算法可以實現,所以我想如何使它更好。由於方程是線性的,因此假設Z始終是第一個要增加的數,一旦找到了解,就沒有必要增加Z.相反,我應該增加Y,將Z設置爲範圍的最小值並且繼續。這樣我可以節省冗餘執行。由於Haskell中沒有循環(至少對我而言),我意識到應該使用遞歸來實現這樣的算法。我通過以下方式實施算法:

solutions :: (Integer -> Integer -> Integer -> Bool) -> Integer -> Integer -> Integer -> Integer -> Integer ->  [(Integer,Integer,Integer)] 
solutions f maxN minN x y z 
    | solved = (x,y,z):nextCall x (y + 1) minN 
    | x >= maxN && y >= maxN && z >= maxN = [] 
    | z >= maxN && y >= maxN = nextCall (x + 1) minN minN 
    | z >= maxN = nextCall x (y + 1) minN 
    | otherwise = nextCall x y (z + 1) 
    where solved = f x y z 
     nextCall = solutions f maxN minN 

triSolve' :: Integer -> Integer -> Integer -> Integer -> Integer -> [(Integer,Integer,Integer)] 
triSolve' a b c d n = 
    let equation x y z = (a * x + b * y + c * z) == d 
     minN = div (-n) 2 
     maxN = div n 2 
    in solutions equation maxN minN minN minN minN 

兩者都產生相同的結果。然而,試圖測量的執行時間產生以下結果:

*Main> length $ triSolve' 2 (-3) (-1) 5 100 
3398 
(2.81 secs, 971648320 bytes) 
*Main> length $ triSolve 2 (-3) (-1) 5 100 
3398 
(1.73 secs, 621862528 bytes) 

意思就是啞巴算法實際上瓶坯比更復雜的一個更好的。基於我的算法是正確的假設(我希望不會變成錯誤:)),我假設第二個算法遭受由遞歸創建的開銷,第一個算法不是因爲它使用列表理解。 有沒有一種方法可以在Haskell中實現比啞巴更好的算法? (另外,我很樂意收到關於我的編碼風格的一般反饋)

+5

測試某些東西的唯一可靠方法是運行一個獨立的可執行文件,通過使用-O2開關進行編譯生成。 ;應用工作者/包裝器轉換:使'nextCall'成爲內部定義,局部於'solutions',因此您不必傳遞不變的參數 - 嵌套定義將訪問外部定義的參數。 –

+0

你的程序是否必須支持任意大的數字?因爲這就是整數所做的。如果你想要效率,並且如果Int的範圍足夠給你,那就用Int!也是,建立與-O2。作爲開始,這應該會改善你的表現。 –

+0

你正在用ghci進行評估 - 這是一個字節碼解釋器,比ghc慢了30倍-O2 –

回答

3

當然有。我們有:

a*x + b*y + c*z = d 

,一旦我們假設值x和y,我們有

a*x + b*y = n 

,其中n是一個數字,我們知道。 因此

c*z = d - n 
z = (d - n)/c 

而且我們只保留積分zs。

+0

謝謝,就是這樣。我很尷尬,我沒有自己想過。我想這是在思考「編碼」時試圖解決數學問題時會發生的情況。不過,我仍然想知道爲什麼triSolve的預製棒比triSolve更差。 – reish

+1

如果這是弗雷格而不是哈斯克爾我會說,無論是傳遞函數作爲參數還是使用編譯器知道的函數 - 例如,它需要3個嚴格的整數參數,性能方面都會產生很大的差異。然而,考慮這個評論是一個瘋狂的猜測,我沒有真正有資格談論Haskell優化...... – Ingo

+0

你能否澄清一下* x + b * y = n怎麼樣? – Dwilson

1

值得注意的是,列表理解由GHC給予特殊處理,並且通常非常快。這可以解釋爲什麼你的triSolve(使用列表理解)比triSolve'(不)要快。

例如,溶液

solve :: Integer -> Integer -> Integer -> Integer -> Integer -> [(Integer,Integer,Integer)] 
-- "Buffalo buffalo buffalo buffalo Buffalo buffalo buffalo..." 
solve a b c d n = 
    [(x,y,z) | x <- vals, y <- vals 
      , let p = a*x +b*y 
      , let z = (d - p) `div` c 
      , z >= minN, z <= maxN, c * z == d - p ] 
    where 
     minN = negate (n `div` 2) 
     maxN = (n `div` 2) 
     vals = [minN..maxN] 

運行我的機器上快速:

> length $ solve 2 (-3) (-1) 5 100 
3398 
(0.03 secs, 4111220 bytes) 

而等效代碼使用do符號寫成:

solveM :: Integer -> Integer -> Integer -> Integer -> Integer -> [(Integer,Integer,Integer)] 
solveM a b c d n = do 
    x <- vals 
    y <- vals 
    let p = a * x + b * y 
     z = (d - p) `div` c 
    guard $ z >= minN 
    guard $ z <= maxN 
    guard $ z * c == d - p 
    return (x,y,z) 
    where 
     minN = negate (n `div` 2) 
     maxN = (n `div` 2) 
     vals = [minN..maxN] 

只要需要兩倍於運行並使用兩倍的內存:

> length $ solveM 2 (-3) (-1) 5 100 
3398 
(0.06 secs, 6639244 bytes) 

有關GHCI中測試的常見警告 - 如果您真的想看到差異,您需要使用-O2編譯代碼並使用體面的基準庫(如Criterion)。

+0

我無法複製這些計時結果。對我來說,列表理解運行速度較慢。 –

+0

這很有趣。你使用什麼版本的GHC?我在7.4.1 –

+0

7.6.3,正如'ghc --version'所報告的那樣。 –