【问题标题】:How to use the memoize function in Data.Function.Memoize如何使用 Data.Function.Memoize 中的 memoize 函数
【发布时间】:2014-06-06 12:21:57
【问题描述】:

在构建自己的记忆表失败后,我转向该类并尝试使用它来加速斐波那契数列的双递归定义:

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 函数提高性能?

【问题讨论】:

    标签: haskell fibonacci memoization


    【解决方案1】:

    显然,只有在您精确地执行一次,然后多次调用它时,记忆化才有用。而在您的方法中,您会一遍又一遍地记住函数。不是这个主意!

    fib' n = memoize fib n
    

    是正确的开始,但由于一个微妙的原因无法按预期工作:它不是 constant applicative form,因为它明确提到了它的论点。

    fib' = memoize fib
    

    是正确的。现在,要真正加快fib 函数的速度,您必须参考记忆的版本。

    fib n = fib' (n-1) + fib' (n-2)
    

    您会看到:只要严格遵守 DRY 并尽可能避免样板文件,您就可以获得正确的版本!

    【讨论】:

    • 感谢您的解释!我以这种方式重写了我原来的(非虚构的)问题,并因此中断了我的记忆。我认为这是因为重新计算发生在一些更高的函数中,这些函数调用了我想存储结果的“昂贵”函数。我的问题涉及元素列表的列表(排列),而昂贵的操作涉及这些元素。我认为结果需要传递。再次感谢,我试着好好利用这个 fib-memo 问题。
    【解决方案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)
    

    现在,我们想要的是一个运算符,它采用这样一个高阶函数并以某种方式“打结”以确保递归调用确实是我们感兴趣的函数。我们可以写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_fixit's part of the memoize package

    【讨论】:

      【解决方案3】:

      memoization 包不会在快速版本中神奇地转换您的功能。它会做的是避免重新计算任何旧的计算:

      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 :-)
      

      【讨论】:

      • 还有一个甚至更通用(和高效)的技术是使用memoize 来避免在递归调用中重新计算......这就是这个问题的意义所在,不是是吗?它比这样的列表或地图黑客要容易得多。 (虽然知道如何做到这一点当然很好。)
      • 我知道序列的优雅递归关系定义,但对于我原来的问题,我需要避免重新计算。
      猜你喜欢
      • 2014-05-15
      • 2010-09-12
      • 2017-11-03
      • 1970-01-01
      • 2010-09-13
      • 1970-01-01
      • 2021-08-20
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多