【问题标题】:optimizing recursive function performance (euler 15: lattice paths)优化递归函数性能(欧拉 15:格路径)
【发布时间】:2013-10-13 12:27:03
【问题描述】:

我正在尝试解决项目欧拉第 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 时它会记住旧的结果吗?

【问题讨论】:

  • 这个问题有非常简单的组合答案。
  • 它不会记住以前的结果。如果你想让你的解决方案工作,你需要建立一些数组并递归调用它的值。
  • 你听说过万无一失的 80 列规则吗?你可以在这里做。 emacswiki.org/emacs/EightyColumnRule

标签: haskell recursion memoization


【解决方案1】:

现在这个问题可以通过将递归关系处理成一个封闭的形式在数学上解决,但我将专注于更有趣的问题,记忆。

首先我们可以使用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

【讨论】:

  • 这是否意味着,在 Haskell 中对于简单的函数根本没有记忆,只能通过将函数应用于某个惰性列表中的下一个值来实现?
  • @sra Haskell 不会记忆,因为它太占用内存了。它确实共享函数参数(例如,通过共享名称调用)但仅此而已
  • 它不是通过在惰性列表中应用函数来记忆的,而是通过填充一个数组,使每个值都依赖于数组中的先前值,这样,惰性确保每个值都只计算一次。跨度>
  • 谢谢,我在想一旦知道值,函数调用实际上会被值“替换”。但是您的惰性列表示例对我来说非常有意义。我也喜欢缓存不需要“外部范围”对象(即使它会被隐藏)这一事实。
  • @sra 没问题,如果你喜欢我的回答,请随时接受 :) 我也推荐使用备忘录组合器,它使用起来非常愉快而且非常可靠。
【解决方案2】:

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

让我们检查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 000 000 000 次。

哎呀。

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

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

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

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

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

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

【讨论】:

    猜你喜欢
    • 2018-10-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-20
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多