【问题标题】:Haskell: memoization of recursion [duplicate]Haskell:递归的记忆
【发布时间】:2018-09-08 18:06:41
【问题描述】:

如果我有以下功能:

go xxs t i
  | t == 0         = 1
  | t < 0          = 0
  | i < 0          = 0
  | t < (xxs !! i) = go xxs t (i-1)
  | otherwise      = go xxs (t - (xxs !! i)) (i-1) + go xxs t (i-1)

记忆结果的最佳方式是什么?我似乎无法理解如何存储一组动态元组并同时更新和返回值。

我在 python 中尝试做的相当于:

def go(xxs, t , i, m):
  k = (t,i)
  if  k in m:      # check if value for this pair is already in dictionary 
      return m[k]
  if t == 0:
      return 1
  elif t < 0:
      return 0
  elif i < 0:
      return 0
  elif t < xxs[i]:
      val = go(xxs, t, i-1,m)  
  else:
      val = (go(xxs, total - xxs[i]), i-1,m) + go(xxs, t, i-1,m)
  m[k] = val  # store the new value in dictionary before returning it
  return val

编辑:我认为这与this answer 有所不同。有问题的函数具有线性进展,您可以使用列表[1..] 对结果进行索引。在这种情况下,我的 Keys (t,i) 不一定是按顺序或增量的。例如,我最终可能会得到一组键,这些键是

[(9,1),(8,2),(7,4),(6,4),(5,5),(4,6),(3,6),(2,7),(1,8),(0,10)]

【问题讨论】:

  • 我想两天前有人问了或多或少相同的问题。
  • “最好的”方法是使用提供记忆的软件包之一。考虑 MemoTrie 和 monad-memo。如果您想扮演自己的角色,使用 State monad 会很有趣且很有启发性。如果您希望您的算法性能更好,请考虑放弃使用 (!!),这本身就是 O(N) 并且会发生多次 - 如果您必须执行随机访问,请使用向量或数组。
  • @WillemVanOnsem 请看我的编辑
  • @ThomasM.DuBuisson 没有更简单的方法可以自己动手。堆栈溢出的其他示例似乎涉及非常复杂的解决方案,似乎我应该很容易......如果你知道如何:)

标签: haskell memoization


【解决方案1】:

有没有更简单的方法来创建自己的 [memoization?]

比什么容易?状态单子真的很简单,如果你习惯于命令式思考,那么它也应该是直观的。

使用向量而不是列表的完整内联版本是:

{-# LANGUAGE MultiWayIf #-}
import Control.Monad.Trans.State as S
import Data.Vector as V
import Data.Map.Strict as M

goGood :: [Int] -> Int -> Int -> Int
goGood xs t0 i0 =
    let v = V.fromList xs
    in evalState (explicitMemo v t0 i0) mempty
 where
 explicitMemo :: Vector Int -> Int -> Int -> State (Map (Int,Int) Int) Int
 explicitMemo v t i = do
    m <- M.lookup (t,i) <$> get
    case m of
        Nothing ->
         do res <- if | t == 0          -> pure 1
                      | t < 0           -> pure 0
                      | i < 0           -> pure 0
                      | t < (v V.! i)   -> explicitMemo v t (i-1)
                      | otherwise       -> (+) <$> explicitMemo v (t - (v V.! i)) (i-1) <*> explicitMemo v t
 (i-1)
            S.modify (M.insert (t,i) res)
            pure res
        Just r  -> pure r

也就是说,如果我们已经计算了结果,我们会在地图中查找。如果是,则返回结果。如果不是,则在返回之前计算并存储结果。

我们可以用几个辅助函数来清理它:

prettyMemo :: Vector Int -> Int -> Int -> State (Map (Int,Int) Int) Int
prettyMemo v t i = cachedReturn =<< cachedEval (
            if | t == 0          -> pure 1
               | t < 0           -> pure 0
               | i < 0           -> pure 0
               | t < (v V.! i)   -> prettyMemo v t (i-1)
               | otherwise       ->
                   (+) <$> prettyMemo v (t - (v V.! i)) (i-1)
                       <*> prettyMemo v t (i-1)
            )
 where
 key = (t,i)
 -- Lookup value in cache and return it
 cachedReturn res = S.modify (M.insert key res) >> pure res

 -- Use cached value or run the operation
 cachedEval oper = maybe oper pure =<< (M.lookup key <$> get)

现在我们的地图查找和地图更新是在一些简单的(对于有经验的 Haskell 开发人员而言)帮助函数中,这些函数包装了整个计算。这里的一个小区别是,无论计算是否以一些较小的计算成本缓存,我们都会更新地图。

我们可以通过删除 monad 来使其更加简洁(请参阅链接的相关问题)。有一个流行的包 (MemoTrie) 可以为您处理胆量:

memoTrieVersion :: [Int] -> Int -> Int -> Int
memoTrieVersion xs = go
 where
 v = V.fromList xs
 go t i | t == 0 = 1
        | t < 0  = 0
        | i < 0  = 0
        | t < v V.! i = memo2 go t (i-1)
        | otherwise   = memo2 go (t - (v V.! i)) (i-1) + memo2 go t (i-1)

如果你喜欢 monadic 风格,你可以随时使用 monad-memo 包。

编辑:将 Python 代码直接翻译成 Haskell 显示了一个重要的区别是变量的不变性。在您的otherwise(或else)情况下,您使用go 两次,并且隐含地一次调用将更新第二次调用使用的缓存(m),从而以记忆的方式节省计算。在 Haskell 中,如果您要避免使用 monad 和惰性求值来递归定义向量(这可能非常强大),那么剩下的最简单的解决方案就是显式传递您的地图(字典):

import Data.Vector as V
import Data.Map as M

goWrapped :: Vector Int -> Int -> Int -> Int
goWrapped xxs t i = fst $ goPythonVersion xxs t i mempty

goPythonVersion :: Vector Int -> Int -> Int -> Map (Int,Int) Int -> (Int,Map (Int,Int) Int)
goPythonVersion xxs t i m =
  let k = (t,i)
  in case M.lookup k m of -- if  k in m:
    Just r -> (r,m)       --     return m[k]
    Nothing ->
      let (res,m') | t == 0 = (1,m)
                   | t  < 0 = (0,m)
                   | i  < 0 = (0,m)
                   | t  < xxs V.! i = goPythonVersion xxs t (i-1) m
                   | otherwise  =
                      let (r1,m1) = goPythonVersion xxs (t - (xxs V.! i)) (i-1) m
                          (r2,m2) = goPythonVersion xxs t (i-1) m1
                      in (r1 + r2, m2)
      in (res, M.insert k res m')

虽然这个版本是 Python 的体面翻译,但我宁愿看到更惯用的解决方案,如下所示。请注意,我们将一个变量绑定到结果计算(为 Int 和更新后的映射命名为“computed”),但由于惰性评估,除非缓存没有产生结果,否则不会做太多工作。

{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE TupleSections #-}
goMoreIdiomatic:: Vector Int -> Int -> Int -> Map (Int,Int) Int -> (Int,Map (Int,Int) Int)
goMoreIdiomatic xxs t i m =
  let cached = M.lookup (t,i) m
      ~(comp, M.insert (t,i) comp -> m')
        | t == 0 = (1,m)
        | t  < 0 = (0,m)
        | i  < 0 = (0,m)
        | t  < xxs V.! i = goPythonVersion xxs t (i-1) m
        | otherwise  =
           let (r1,m1) = goPythonVersion xxs (t - (xxs V.! i)) (i-1) m
               (r2,m2) = goPythonVersion xxs t (i-1) m1
           in (r1 + r2, m2)
    in maybe (comp,m') (,m) cached

【讨论】:

  • 谢谢你。 “比什么更容易?”,好吧,我的意思是像我的 python 代码中的 50 个左右的字符修改一样简单。该代码的记忆力非常小。我希望 haskell 版本不会涉及到融化我的大脑:)。但是,您发布的内容非常有帮助,这正是我在学习方面所寻找的。再次感谢
  • @matthias 啊,我明白了。我添加了您的 Python 代码的翻译,以便您阅读和比较。
  • 太棒了,非常感谢您的帮助!
  • memoTrie 版本中的v = V.toList xs 也应该是v = V.FromList xs,你的goPythonVersion 似乎不起作用?
  • 哦,哈哈。那是因为我从不将结果插入到地图中。我昨天在想什么! python m[k]=val 没有匹配项。稍后将进行编辑。
猜你喜欢
  • 2022-01-04
  • 1970-01-01
  • 2017-06-25
  • 1970-01-01
  • 2012-11-12
  • 2012-09-27
  • 1970-01-01
  • 2022-01-19
  • 2019-11-29
相关资源
最近更新 更多