优化递归函数性能(euler 15:晶格路径)

时间:2013-10-13 12:27:04

标签: haskell recursion memoization

我正在尝试解决项目eulers的第15个问题,格子路径(http://projecteuler.net/problem=15)。

我的第一次尝试是逐行解决问题,然后取最后一行中的最后一个元素。

number_of_ways_new_line last_line = foldl calculate_values [] last_line
                                    where
                                      calculate_values lst x = lst ++ [(if length lst > 0 then (last lst) + x else head last_line)]

count_ways x = foldl (\ lst _ -> number_of_ways_new_line lst) (take x [1,1..]) [1..x-1]

result_15 = last $ count_ways 21

这很有效,而且速度很快,但我觉得这很难看。所以我想了一会儿,想出了一个更惯用的功能(如果我弄错了,请纠正我),使用递归来解决问题:

lattice :: Int -> Int -> Int
lattice 0 0 = 1
lattice x 0 = lattice (x-1) 0
lattice 0 y = lattice (y-1) 0
lattice x y
  | x >= y    = (lattice (x-1) y) + (lattice x (y-1))
  | otherwise = (lattice y (x-1)) + (lattice (y-1) x)

这适用于短数字,但它根本不能缩放。我使用lattice 1 2lattice 2 1始终相同的事实对其进行了优化。为什么这么慢?难道Haskell不会记住以前的结果,所以每当调用lattice 2 1时它会记住旧的结果吗?

2 个答案:

答案 0 :(得分:2)

现在这个问题可以在数学上解决,将重复关系操作为封闭形式,但我会关注更有趣的问题,记忆。

首先我们可以使用Data.Array(这是懒惰的)

 import Data.Array as A

 lattice x y = array ((0, 0), (x, y)) m ! (x, y)
   where m = [(,) (x, y) (lattice' x y) | x <- [0..x], y <- [0..y]
         lattice' 0 0  = 1
         lattice' x 0 = lattice (x-1) 0
         lattice' 0 y = lattice (y-1) 0
         lattice' x y | x >= y    = (lattice (x-1) y) + (lattice x (y-1))
                      | otherwise = (lattice y (x-1)) + (lattice (y-1) x)

现在这些重复遍历地图,但是,由于地图是懒惰的,一旦评估了地图条目,它的thunk将变为一个简单的值,确保它只计算一次。

我们也可以使用精彩的memo-combinators库。

 import Data.MemoCombinators as M
 lattice = memo2 M.integral M.integral lattice'
   where lattice' 0 0 = 1
         lattice' x 0 = lattice (x-1) 0
         lattice' 0 y = lattice (y-1) 0
         lattice' x y | x >= y    = lattice (x-1) y + lattice x (y-1)
                      | otherwise = lattice y (x-1) + lattice (y-1) x

答案 1 :(得分:1)

这个答案确实很慢,因为它同时依赖于多次递归。

让我们检查lattice函数。它有什么作用?它返回两个lattice函数的总和,另一个lattice函数或第一个函数。

现在让我们举一个例子:lattice 2 2 这等于lattice 1 2 + lattice 2 1 这等于lattice 0 2 + lattice 1 1 + lattice 1 1 + lattice 2 0 这等于......

你真的意识到了这个问题吗?在代码中没有一个乘法或添加常量的情况。这意味着整个功能将归结为:

1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + ... + 1

这笔钱是你的最终结果。但是,上述总和中的每1个都是一个或多个函数调用的结果。因此,您的功能将比结果更有价值。

现在考虑lattice 20 20产生大约1500亿(这是一个估计,所以我不会破坏太多)。

这意味着您的功能将被评估大约150亿次。

让人惊讶。

不要为这个错误感到难过。我曾经说过:

fibbonaci x = fibbonaci (x-1) + fibbonaci (x-2)

我鼓励你弄清楚为什么这是个坏主意。

PS很高兴他们没有要求lattice 40 40。那将会超过10 ^ 23个函数调用,或者至少300万年。

PPS通过仅计算一侧的加速度只会减慢执行速度,因为函数调用的数量不会减少,但每个函数调用的时间会减少。

免责声明:这是我见过的第一块Haskell代码。