以下是我对此问题的第一次尝试。它基本上是我的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中更好地进行动态编程。)
答案 0 :(得分:4)
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)
参数,了解如何将fIter
调用到f
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]
我们可以通过将fIter
中定义的fix :: (a -> a) -> a
修改为fix f = let x = f x in x
来恢复原来的慢fIter :: Int -> Int -> Int
fIter = fix fIter'
。
fIter
现在我们处于改变Data.MemoTrie
自称的方式的好地方。另一个"懒惰的数据结构,必要时填写"在Data.Function
包中的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)
和trie
的实例。
Data.MemoTrie
的 fIter
只接受一个参数,因此我们需要将fIter'
的两个参数打包到一个参数中。我们通常使用MemoTrie执行此操作,但是,由于uncurryOr
需要采用与其结果相同的类型作为参数,我们还需要uncurry
递归调用解包参数。 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
会产生令人满意的结果,import Data.MemoTrie
memoOR :: HasTrie a => ((a -> b) -> a -> b) ->
a -> b
memoOR or = f
where
f = untrie t
t = trie (or f)
。
为了记住一般的开放递归函数,我们为开放递归函数构建一个trie,传递trie查找函数应该如何得到它的递归结果。
memoOR
我们可以从fix
memo = untrie . trie
和Data.MemoTrie
更优雅地撰写memoOR or = fix (memo . or)
。
fIter
我们现在可以定义如何有效地计算fIter :: Int -> Int -> Int
fIter = curry . memoOR . uncurryOR $ fIter'
{{1}}