【问题标题】:Haskell, memoization, stack overflowHaskell,记忆,堆栈溢出
【发布时间】:2011-11-06 23:48:00
【问题描述】:

我正在处理 Project Euler (http://projecteuler.net/problem=14) 的问题 14。我正在尝试使用记忆化,以便将给定数字的序列长度保存为部分结果。为此,我正在使用 Data.MemoCombinators。下面的程序会产生堆栈溢出。

import qualified Data.MemoCombinators as Memo

sL n = seqLength n 1
seqLength = Memo.integral seqLength'
  where seqLength' n sum = if (n == 1)     then sum
                           else if (odd n) then seqLength (3*n+1) (sum+1)
                           else                 seqLength (n `div` 2) (sum+1)

p14 = snd $ maximum $ zip (map sL numbers) numbers
  where numbers = [1..max]
        max     = 999999

堆栈溢出应该是由于 sum+1 被懒惰地评估。如何在每次调用 seqLength 之前强制对其进行评估?顺便说一句,memoization 实施得好吗?比起解决练习,我更感兴趣的是指出我的 Haskell 错误。

【问题讨论】:

  • 顺便说一句,对于这个问题,您可能希望使用arrayRange 组合器——因为序列中的值变得非常大,备忘录查找变得更加昂贵(integral 的日志时间)和不太可能被重复使用。对于足够大的值,最好忽略记忆。

标签: haskell stack-overflow memoization


【解决方案1】:

最常见的强制评估方法是使用seq$! 或爆炸模式。然而sum+1 并不是这里的罪魁祸首。 maximum 是。用更严格的foldl1' max 替换它可以修复堆栈溢出错误。

已经解决了,原来你在这里的记忆不好。 Memo.integral 只记住第一个参数,所以你正在记住 seqLength' 的部分应用程序,这并没有真正做任何有用的事情。您应该在没有尾递归的情况下获得更好的结果,以便您记住实际结果。此外,正如 luqui 指出的那样,arrayRange 在这里应该更高效:

seqLength = Memo.arrayRange (1, 1000000) seqLength'
  where seqLength' 1 = 1
        seqLength' n | odd n     = 1 + seqLength (3*n+1)
                     | otherwise = 1 + seqLength (n `div` 2)

【讨论】:

  • @rturrado:它在Data.List 中定义。对于查找函数,我建议使用Hoogle
  • 好的,这是一个正确的答案。我在回复中看到的所有代码对我来说似乎都很密集。只需几行代码就可以学到很多东西。后台发生了这么多事情。仍然要理解 memoization,Memo.arrayRange...顺便说一句,我定义seqLength :: Int -> Int 并且仍然产生堆栈溢出,但我已经检查了为什么在同一个问题的另一个答案中会发生这种情况:link
【解决方案2】:

我对 Data.MemoCombinators 不熟悉,所以一般建议是:尝试seqLength (3*n+1) $! (sum+1)(当然,n 也是如此)。

【讨论】:

    【解决方案3】:

    既然我们可以利用惰性,为什么还要使用 MemoCombinators?诀窍是做类似的事情

    seqLength x = lengths !! x - 1
       where lengths = map g [1..9999999]
             g n | odd n = 1 + seqLength (3 * n + 1)
                 | otherwise = 1 + seqLength (n `div` 2)
    

    这应该以一种记忆的方式工作。 [改编自 @hammar 的非尾递归解决方案]

    当然,对于记忆的情况,seqLength 是 O(n),因此它的性能会降低。不过,这是可以补救的!我们只是利用了 Data.Vector 是流式传输并且具有 O(1) 随机访问的事实。 fromList 和 map 将同时完成(因为我们使用的是盒装向量,所以 map 将简单地生成 thunk 而不是实际值)。我们还使用非记忆版本,因为我们不可能记忆所有可能的值。

    import qualified Data.Vector as V
    
    seqLength x | x < 10000000 = lengths V.! x - 1
                | odd x = 1 + seqLength (3 * n + 1)
                | otherwise = 1 + seqLength (n `div` 2)
       where lengths = V.map g $ V.fromList [1..99999999]
             g n | odd n = 1 + seqLength (3 * n + 1)
                 | otherwise = 1 + seqLength (n `div` 2)
    

    这应该与使用 MemoCombinators 相当或更好。这台计算机上没有 haskell,但如果你想知道哪个更好,有一个名为 Criterion 的库非常适合这类事情。

    我认为使用 Unboxed Vectors 实际上可以提供更好的性能。当您评估一项时(我认为),它会立即强制执行所有操作,但无论如何您都需要这样做。因此,您可以只运行foldl' max 来获得O(n) 解决方案,该解决方案应该具有更少的恒定开销。

    【讨论】:

    • 请注意,这将超出界限,因为 Collat​​z 序列在回到 1 之前往往会上升很多。例如,seqLength 999999 将尝试评估 seqLength 2999998,这将失败,因为向量/列表只有 1000000 个元素长。
    • @hammar 是的,我后来意识到了这一点。也许只有记忆,比如说,1000 万,如果超过了,不要使用记忆版本?我放置了一个不使用警卫的后备非记忆版本。不过,我觉得这个方法还是蛮适用的。
    • @monadic:但是,现在您的代码等同于将 Memo.integral 替换为 Memo.arrayRange (1, 1000000),它只为您做这个 only-memoize-if-within-bounds。主要区别在于它使用array 而不是vector
    • @hammar 对于消除依赖关系来说还不错:P
    【解决方案4】:

    如果记忆有用,对于这个问题,你根本不需要记忆。只需使用 foldl' 和 bang 模式:

    snd $ foldl' (\a n-> let k=go n 1 in if fst a < .... 
      where go n !len | n==1 =  ....
    

    使用 -O2 -XBangPatterns 编译。运行 stadalone 总是更好,因为在 ghci 中运行编译后的代码会导致空间泄漏。

    【讨论】:

      猜你喜欢
      • 2011-08-22
      • 1970-01-01
      • 2019-07-08
      • 2015-08-05
      • 1970-01-01
      • 2019-05-18
      • 2020-06-06
      • 2011-11-23
      • 2015-01-07
      相关资源
      最近更新 更多