【问题标题】:Memoize Project Euler 116 in Haskell在 Haskell 中记忆 Project Euler 116
【发布时间】:2015-04-16 09:20:01
【问题描述】:

下面是我第一次尝试解决这个问题。它基本上是我的 C++ 解决方案的音译,减去了用于记忆函数 f 的结果的全局哈希表。

f :: Int -> Int -> Int
f blocksize spaces
    | blocksize > spaces = 0
    | blocksize == spaces = 1
    | otherwise = 1 + fIter blocksize (spaces - blocksize)

fIter :: Int -> Int -> Int
fIter blocksize spaces =
    sum $ map (f blocksize) [1..spaces]

main :: IO ()
main = do
    let spaces = 50
    print $ fIter 2 spaces + fIter 3 spaces + fIter 4 spaces

我想我理解创建一个根据需要填充的惰性数据结构的基本思想,就像在this type of thing 中一样,但这里让我感到困惑的是,有两个相互递归的函数相互调用而不是一个.我已经尝试将它们组合成一种形式,如下所示,但还没有任何运气直接在我的脑海中。有什么建议吗?

fIter2 :: Int -> Int -> Int
fIter2 b s = sum $ map (\i -> memos ! i) [1..s]
    where
        memos = listArray (1, s) (map (f b) [1..s])
        f blocksize spaces
          | blocksize > spaces = 0
          | blocksize == spaces = 1
          | otherwise = 1 + fIter2 blocksize (spaces - blocksize)

(我知道这个问题有一个组合解决方案,但我现在更感兴趣的是在 Haskell 中更好地进行动态编程。)

【问题讨论】:

    标签: haskell dynamic-programming


    【解决方案1】:

    f 是一个非常简单的函数。假设fIter 易于计算,f 也将易于计算。我们不需要记住f。我们唯一的目标是记住fIter

    当开始在 Haskell 中记忆递归函数时,以开放递归形式编写函数很有用。函数的开放递归形式删除了显式递归,而是添加了一个额外的参数来说明当函数需要递归调用自身时要做什么。这将为我们提供一种方法来改变函数递归调用自身的方式。

    我们将fIter的签名从第一种类型更改为第二种类型

                          The type of fIter
                           v
                           Int -> Int -> Int
    (Int -> Int -> Int) -> Int -> Int -> Int
     ^                     ^
     |                     Gets back something with the type of fIter
     What to do when fIter needs to call itself recursively
    

    fIter通过f递归调用自己,所以我们首先将(Int -> Int -> Int)参数添加到fIterf的调用方式

    f' :: (Int -> Int -> Int) -> Int -> Int -> Int
    f' r blocksize spaces
        | blocksize > spaces = 0
        | blocksize == spaces = 1
        | otherwise = 1 + r blocksize (spaces - blocksize)
    

    修改后的fIter' 会将如何递归调用自身(它从新参数中获得)作为新参数传递给f'

    fIter' :: (Int -> Int -> Int) -> Int -> Int -> Int
    fIter' r blocksize spaces =
        sum $ map (f' r blocksize) [1..spaces]
    

    我们可以通过将Data.Function 中定义的fix :: (a -> a) -> a 修复为fix f = let x = f x in x 来恢复原来的慢速fIter

    fIter :: Int -> Int -> Int
    fIter = fix fIter'
    

    现在我们可以改变fIter 的自称方式了。在MemoTrie 包中的Data.MemoTrie 中提供了另一种“根据需要填充的惰性数据结构”。它提供了两个我们感兴趣的功能。 trie :: HasTrie a => (a -> b) -> a :->: b 构建从提供的函数构建的惰性数据结构。 untrie :: HasTrie a => (a :->: b) -> a -> b 返回一个从惰性数据结构中查找参数的函数。这些仅适用于可以构建惰性数据结构的某些参数类型HasTrie a。幸运的是,对于我们的问题,已经有 HasTrie Int(HasTrie a, HasTrie b) => HasTrie (a, b) 的实例。

    来自Data.MemoTrietrie 只接受一个参数,因此我们需要将fIter 的两个参数打包成一个参数。我们通常会使用uncurry 执行此操作,但是,由于fIter' 需要将与结果相同类型的东西作为参数,我们还需要curry 递归调用来解包参数。 uncurryOr 将取消任何打开的递归函数。

    uncurryOR :: (( a -> b  -> c) ->  a -> b  -> c) ->
                  ((a,   b) -> c) -> (a,   b) -> c
    uncurryOR or = uncurry . or . curry
    

    将此应用于fIter' 会产生令人满意的结果,uncurryOR fIter' :: ((Int, Int) -> Int) -> (Int, Int) -> Int

    一般来说,为了记忆一个开放递归函数,我们为开放递归函数构建一个 trie,传入 trie 查找作为函数应如何获得其递归结果。

    import Data.MemoTrie
    
    memoOR :: HasTrie a => ((a -> b) -> a -> b) ->
                                        a -> b
    memoOR or = f
        where
            f = untrie t
            t = trie (or f)
    

    我们可以从Data.MemoTrie 中将fixmemo = untrie . trie 写成更优雅的memoOR

    memoOR or = fix (memo . or)
    

    我们现在可以定义如何有效地计算fIter

    fIter :: Int -> Int -> Int
    fIter = curry . memoOR .  uncurryOR $ fIter'
    

    【讨论】:

    • 感谢您的详细回复。不过我需要一点时间来消化它。
    猜你喜欢
    • 2011-03-23
    • 2011-02-26
    • 2023-03-21
    • 2012-07-09
    • 1970-01-01
    • 2016-04-11
    • 2020-10-25
    • 2011-03-13
    • 1970-01-01
    相关资源
    最近更新 更多