dynamic编程algorithm如何在惯用的Haskell中实现?

Haskell和其他函数式编程语言是围绕不维护状态而build立的。 我对于函数式编程的工作原理和概念还不熟悉,所以我想知道是否可以用FP方式实现DPalgorithm。

什么function的编程结构可以用来做到这一点?

通常的做法是通过懒惰的记忆。 从某种意义上说,recursion斐波那契函数可以被认为是dynamic规划,因为它可以计算重叠子问题的结果。 我意识到这是一个疲惫的例子,但这里有一个口味。 它使用data-memocombinators库来进行懒惰的记忆。

import qualified Data.MemoCombinators as Memo fib = Memo.integral fib' where fib' 0 = 0 fib' 1 = 1 fib' n = fib (n-1) + fib (n-2) 

fib是memoized版本,而fib'只是“蛮力”的问题,但是使用memoized的fib来计算它的子问题。 其他的DPalgorithm是用相同的风格编写的,使用不同的logging结构,但是同样的想法只是以直接的函数方式和记忆来计算结果。

编辑 :我终于投入,并决定提供一个memoizable typeclass。 这意味着现在的记忆更容易:

 import Data.MemoCombinators.Class (memoize) fib = memoize fib' where fib' :: Integer -> Integer -- but type sig now required ... 

需要按照types的Instaead,你可以只是memoize任何东西。 如果你喜欢,你仍然可以使用旧的方式。

Rabhi和Lapalme的algorithm:函数式编程方法在这方面有很好的一章,说明了一些FP概念正在被使用,即高阶函数懒惰评估 。 我认为我可以重现其高阶函数的简化版本。

它被简化,它只适用于将Int作为input并生成Int作为输出的函数。 因为我们以两种不同的方式使用Int,所以我会为它们“Key”和“Value”制作同义词。 但不要忘记,因为这些是synonynms,所以使用Keys和Values是完全可能的,反之亦然。 他们只用于可读性。

 type Key = Int type Value = Int dynamic :: (Table Value Key -> Key -> Value) -> Key -> Table Value Key dynamic compute bnd = t where t = newTable (map (\coord -> (coord, compute t coord)) [0..bnd]) 

我们稍微剖析一下这个函数。

首先,这个function是做什么的? 从types签名我们可以看到,它以某种方式操纵表。 事实上,第一个参数“计算”是一个函数(因此dynamic是一个“高阶”函数),它从表中产生某种价值,第二个参数只是某种上限,告诉我们在哪里停止。 而作为输出,“dynamic”function给了我们一些表。 如果我们想得到一些DP友好问题的答案,我们运行“dynamic”,然后从我们的表中查找答案。

要使用这个函数来计算斐波那契,我们可以像这样运行它

 fib = findTable (dynamic helper n) n where helper ti = if i <= 1 then i else findTable t (i-1) + findTable t (i-2) 

现在不要太担心理解这个函数。 当我们探索“dynamic”时,它会变得更清晰。

其次,我们需要知道什么样的先决条件来了解这个function? 我假设你对语法或多或less熟悉语法,[0..x]表示从0到x的列表, – >types签名如Int – > Table – > …与 – >匿名function,如\ coord – > …如果你对这些不熟悉,他们可能会阻碍。

这个查找表的另一个先决条件是。 我们不想担心它是如何工作的,但是让我们假设我们可以从键值对列表中创build它们,并且在其中查找条目:

 newTable :: [(k,v)] -> Table vk findTable :: Table vk -> k -> v 

这里要注意三点:

  • 为了简单起见,我们没有使用Haskell标准库中的等价物
  • 如果您要求查找表中不存在的值,findTable将会崩溃。 如果需要,我们可以使用更好的版本来避免这种情况,但这是另一个post的主题
  • 奇怪的是,即使书本和标准的Haskell库提供了一个,我也没有提到任何types的“为表添加值”函数。 为什么不?

最后, 这个函数是如何工作的? 这里发生了什么? 我们可以放大一点function的肉,

 t = newTable (map (\coord -> (coord, compute t coord)) [0..bnd]) 

并有条不紊地把它拆开。 从外部开始,我们已经得到了t = newTable(…),这似乎告诉我们,我们正在从某种列表创build一个表。 无聊。 那么名单呢?

 map (\coord -> (coord, compute t coord)) [0..bnd] 

在这里,我们已经得到了更高阶的map函数,从0到bnd列表,并产生一个新的列表。 要计算新的列表,它使用一个函数\ coord – >(coord,compute t coord)。 记住上下文:我们试图从关键值对构build一个表,所以如果你研究元组,第一部分坐标必须是关键,第二部分的坐标必须是值。 第二部分是事情令人兴奋的地方。 让我们进一步放大

 compute t coord 

我们正在build立一个来自键值对的表格,我们插入这些表格的值来自运行“计算t坐标”。 之前我没有提到的一件事情是,计算需要一个表格和一个关键字作为input,并告诉我们应该在表格中插入什么值,换句话说,我们应该关联哪个关键值。 然后,将这个想法带回到dynamic编程,就是计算函数使用表中以前的值来计算我们应该插入的新值。

就这样! 要在Haskell中进行dynamic编程,我们可以通过使用从表中查找先前值的函数连续地将值插入单元格来构build某种表。 很简单,对吧?还是这样?

也许你有和我一样的经历。 所以我想分享一下这个function的当前进展。 当我第一次读这个函数的时候,似乎有一种直观的感觉,我没有多想。 然后,我仔细阅读,做了一个双重采取,等待什么?! 这怎么可能工作? 再来看看这段代码。

 compute t coord 

为了计算给定单元格的值并填充表格,我们首先传递一个我们试图创build的表格。 如果您指出函数式编程是关于不可变性的,那么如何使用我们尚未计算的值的这个业务可能还有效? 如果你有一点点的FP在你的腰带上,你可能会像我一样问自己:“这是一个错误吗?”,这不应该是一个“折叠”,而不是一个“地图”?

这里的关键是懒惰的评价。 一点点的魔力,使得从它自己的位创造一个不可改变的价值成为可能。 作为一个长期的黄色带Haskeller,我仍然觉得懒惰的概念有点莫名其妙。 所以我必须让其他人接pipe。

同时,我只是告诉自己,这是好的。 我满足于把桌子想像成一个有许多箭头的圆点。 以fib为例:

 o | |--0--> 1 | |--1--> 1 | |--2--> 2 | |--3--> 2 . . . 

我们还没有看到的桌子是未被发现的领域。 当我们首先走下来的时候,这一切都是未被发现的

 o . . . 

当我们想计算第一个值时,我们不需要更多地了解表格,因为i <= 1。

  helper ti = if i <= 1 then i else findTable t (i-1) + findTable t (i-2) o | |--0--> 1 . . . 

当我们要计算连续的值时,我们总是只回顾已经发现的表格部分(dynamic编程,嘿,嘿!)。 关键要记住的是,我们在这里百分之百地坚持不变的价值观,除了懒惰外没有什么奇特的花招。 “t”确实意味着表格,而不是“第42次迭代时的表格处于当前状态”。 只是我们只是发现桌子上的那些东西,告诉我们与42相对应的价值是什么时候我们真正要求的。

希望与其他人在StackOverflow,你会走得比我更远,不要留下隐约喃喃自语“呃耶,懒惰的东西或其他”这真的不是一个大问题:-)

如果你想使用2或3参数的DP(例如,在处理string时),你可以使用不可变数组:

 import Data.Array.IArray answer :: String -> Int answer s = table ! (1, l) where l = length s --signatyres are needed, because GHC doesn't know what kind of Array we need --string is stored in Array because we need quick access to individual chars a :: Array Int Char a = listArray (1, l) s table :: Array (Int, Int) Int table = listArray ((1, 1), (l, l)) [fij | i <- [1..l], j <- [1..l]] fij | i > j = 0 | i == j = 1 | (a ! i) == (a ! j) = 2 + table ! (i+1, j-1) | otherwise = maximum [table ! (i+1, j), table ! (i, j-1)] 

这个代码解决了以下任务:给定一个stringS,find最大长度的S的子序列,这将是一个混合体(子序列不需要是连续的)。

基本上,'f'是resursive函数,array'table'是所有可能值的matrix。 因为Haskell是懒惰的,只需要计算出'f'的答案值。 换句话说,这是与memoizationrecursion。 所以使用Data.Memocombinators,这是一样的,但已经被别人写了:)

在haskell中的dynamic编程可以用懒惰来expression,请看本页的第一个例子

dynamic编程algorithm通常利用将问题简化为简单问题的思想。 它的问题可以表述为一些基本的事实(比如从方形单元到它自己的最短path长度为0)加上一组循环规则,这些规则恰好显示了如何减less问题“find从单元(i,j)到单元(0,0)到问题”find从单元(i-1,j)(i,j-1)(0,0)最短path;select最好的“ 。 AFAIK这可以很容易地expression在function风格的程序; 没有涉及国家。