在未能构建我自己的memoization表之后,我转向了所述类并尝试使用它来加速Fibonacci序列的双递归定义:
fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)
我尝试过几种方式使用该类的memoize
函数,但即使是下面的构造也似乎慢(在fib 33
吃10秒):
fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib n = memoize fib (n-1) + memoize fib (n-2)
fib' :: Int -> Int
fib' n = memoize fib n
我尝试过以其他方式发布memoize
,但性能似乎没有提高。
我知道有其他方法来解决这个问题,特别是计算得更有效,但对于我原来的问题,我想使用Memoize包。所以我的问题是,如何使用此包中的memoize
函数来提高性能?
答案 0 :(得分:6)
显然,只有在 之后才进行备忘,然后多次调用。而在你的方法中,你一遍又一遍地记住这个功能。这不是主意!
fib' n = memoize fib n
是一个正确的开始,但由于一个微妙的原因而无法按照需要工作:它不是constant applicative form,因为它明确提到了它的论点。
fib' = memoize fib
是对的。现在,为了实际加速fib
功能,您必须参考记忆版本。
fib n = fib' (n-1) + fib' (n-2)
你明白:严格遵守DRY并避免尽可能多的样板文件让你找到正确的版本!
答案 1 :(得分:2)
正如其他人所说,问题是您正在记忆对该函数的顶级调用,但您没有使用该信息来避免重新计算递归调用。让我们看看我们如何确保递归调用也被缓存。
首先,我们有明显的重要性。
import Data.Function.Memoize
然后我们将描述函数fib的调用图。为此,我们编写了一个更高阶函数,它使用其参数而不是递归调用:
fib_rec :: (Int -> Int) -> Int -> Int
fib_rec f 0 = 0
fib_rec f 1 = 1
fib_rec f n = f (n - 1) + f (n - 2)
现在,我们想要的是一个操作员,它具有如此高阶的功能,并以某种方式“结合”#34;确保递归调用确实是我们感兴趣的函数。我们可以编写fix
:
fix :: ((a -> b) -> (a -> b)) -> (a -> b)
fix f = f (fix f)
然后我们回到了一个效率低下的解决方案:我们永远不会忘记任何事情。另一种解决方案是编写一些看起来像修复的东西,但确保记忆在整个地方发生。我们称之为memoized_fix
:
memoized_fix :: Memoizable a => ((a -> b) -> (a -> b)) -> (a -> b)
memoized_fix = memoize . go
where go f = f (memoized_fix f)
现在你有了有效的功能fib_mem
:
fib_mem :: Int -> Int
fib_mem = memoized_fix fib_rec
你甚至不必自己写memoized_fix
,it's part of the memoize package。
答案 2 :(得分:0)
备忘录软件包不会在快速版本中神奇地转换您的功能。它将做什么是避免重新计算任何旧的计算:
import Data.Function.Memoize
fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)
fib_mem = memoize fib
val = 40
main = do
print $ fib val -- slow
print $ fib val -- slow
print $ fib_mem val -- slow
print $ fib_mem val -- fast!
您需要的是一种避免在递归调用中重新计算任何值的方法。一个简单的方法是将斐波那契序列计算为无限列表,取第n个元素:
fibs :: [Int]
fibs = 0:1:(zipWith (+) fibs (tail fibs))
fib_mem n = fibs !! n
更通用的技术是携带Map Int Int并将结果插入其中。
fib :: (Map Int Int) -> Int -> (Int, Map Int Int)
-- implementation left as exercise :-)