我想用Haskell編寫一個高效的Floyd-Warshall實現方法,使用Vector
s來希望獲得良好的性能。Haskell中Floyd-Warshall的性能 - 修復空間泄漏
實現非常簡單,但不是使用三維| V |×| V |×| V |矩陣,使用2維向量,因爲我們只讀過以前的k
值。
因此,該算法實際上只是傳遞2D矢量並生成新的2D矢量的一系列步驟。最終的2D矢量包含所有節點(i,j)之間的最短路徑。
我的直覺告訴我,這將確保之前的2D載體的每一步之前評估是很重要的,所以我就prev
參數的fw
功能和嚴格foldl'
使用BangPatterns
:
{-# Language BangPatterns #-}
import Control.DeepSeq
import Control.Monad (forM_)
import Data.List (foldl')
import qualified Data.Map.Strict as M
import Data.Vector (Vector, (!), (//))
import qualified Data.Vector as V
import qualified Data.Vector.Mutable as V hiding (length, replicate, take)
type Graph = Vector (M.Map Int Double)
type TwoDVector = Vector (Vector Double)
infinity :: Double
infinity = 1/0
-- calculate shortest path between all pairs in the given graph, if there are
-- negative cycles, return Nothing
allPairsShortestPaths :: Graph -> Int -> Maybe TwoDVector
allPairsShortestPaths g v =
let initial = fw g v V.empty 0
results = foldl' (fw g v) initial [1..v]
in if negCycle results
then Nothing
else Just results
where -- check for negative elements along the diagonal
negCycle a = any not $ map (\i -> a ! i ! i >= 0) [0..(V.length a-1)]
-- one step of the Floyd-Warshall algorithm
fw :: Graph -> Int -> TwoDVector -> Int -> TwoDVector
fw g v !prev k = V.create $ do -- ← bang
curr <- V.new v
forM_ [0..(v-1)] $ \i ->
V.write curr i $ V.create $ do
ivec <- V.new v
forM_ [0..(v-1)] $ \j -> do
let d = distance g prev i j k
V.write ivec j d
return ivec
return curr
distance :: Graph -> TwoDVector -> Int -> Int -> Int -> Double
distance g _ i j 0 -- base case; 0 if same vertex, edge weight if neighbours
| i == j = 0.0
| otherwise = M.findWithDefault infinity j (g ! i)
distance _ a i j k = let c1 = a ! i ! j
c2 = (a ! i ! (k-1))+(a ! (k-1) ! j)
in min c1 c2
但是,使用具有47978邊的1000節點圖運行此程序時,事情看起來並不好。內存使用率非常高,程序運行時間過長。該計劃編制了ghc -O2
。
我重建的程序的分析,和有限的迭代次數以50:
results = foldl' (fw g v) initial [1..50]
我然後運行該程序與+RTS -p -hc
和+RTS -p -hd
:
這是...有趣的,但我想它表明它是acc模擬了一大堆thunk。不好。
好了,在黑暗中拍了幾張照片後,我在fw
增加了deepseq
確保prev
真的是evaluted:
let d = prev `deepseq` distance g prev i j k
現在事情看起來更好,我可以實際運行的程序以恆定的內存使用完成。顯而易見的是,關於prev
的論點是不夠的。
爲了與以前的曲線圖進行比較,這裏是50次迭代的內存使用量添加deepseq
後:
好了,這樣的事情是更好,但我仍然有一些問題:
- 這是這個空間泄漏的正確解決方案嗎?我錯誤地認爲插入
deepseq
有點難看? - 我在這裏的用法是
Vector
s這裏習慣用語/正確嗎?我爲每次迭代構建了一個全新的向量,並希望垃圾收集器將刪除舊的Vector
。 - 有沒有其他的事情可以做,使這種方法運行更快?
對於引用,這裏是graph.txt
:http://sebsauvage.net/paste/?45147f7caf8c5f29#7tiCiPovPHWRm1XNvrSb/zNl3ujF3xB3yehrxhEdVWw=
這裏是main
:
main = do
ls <- fmap lines $ readFile "graph.txt"
let numVerts = head . map read . words . head $ ls
let edges = map (map read . words) (tail ls)
let g = V.create $ do
g' <- V.new numVerts
forM_ [0..(numVerts-1)] (\idx -> V.write g' idx M.empty)
forM_ edges $ \[f,t,w] -> do
-- subtract one from vertex IDs so we can index directly
curr <- V.read g' (f-1)
V.write g' (f-1) $ M.insert (t-1) (fromIntegral w) curr
return g'
let a = allPairsShortestPaths g numVerts
case a of
Nothing -> putStrLn "Negative cycle detected."
Just a' -> do
putStrLn $ "The shortest, shortest path has length "
++ show ((V.minimum . V.map V.minimum) a')
側備註:'任何不$地圖(!\ I - >一個I I> = 0)[0 ..(V.length A-1)]'只是'任何(\ i - > a!i!i <0)[0 ..(V.length a-1)]'。 –
你試過用可變的向量重寫你的'foldl''和'forM_'計算爲顯式循環嗎? (例如[在'test0'這裏](http://codereview.stackexchange.com/a/24968/9064),儘管使用數組,而不是使用矢量。[和循環代替通常的'forM']( http://stackoverflow.com/a/15026238/849891)) –
@WillNess:不,我嘗試的唯一方法是用一個嚴格的累加器替換帶有尾遞歸函數的'foldl',但似乎沒有有效果。看到你鏈接到的兩個例子都充斥着「不安全的」函數 - 我真的希望能夠在不訴諸於此的情況下實現合理的性能,這是有點令人沮喪的。 :-) – beta