我更多地考虑用递归方案来编码这个问题。也许有一个很好的方法来解决无序问题(即,考虑 5c + 1c 与 1c + 5c 不同),使用组织同态来缓存无向递归调用,但我不知道它是什么。相反,我寻找一种使用递归方案来实现动态编程算法的方法,其中以特定顺序探测搜索树,以便确保您不会多次访问任何节点。
我使用的工具是 hylomorphism,它会在您正在阅读的文章系列的后面部分出现。它由展开(变形)和折叠(变形)组成。 hylomorphism 使用 ana 构建中间结构,然后使用 cata 将其分解为最终结果。在这种情况下,我使用的中间结构描述了一个子问题。它有两个构造函数:要么子问题已经解决,要么还剩下一些钱可以找零,以及可以使用的硬币面额池:
data ChangePuzzle a = Solved Int
| Pending {spend, forget :: a}
deriving Functor
type Cent = Int
type ChangePuzzleArgs = ([Cent], Cent)
我们需要一个将单个问题转化为子问题的余代数:
divide :: Coalgebra ChangePuzzle ChangePuzzleArgs
divide (_, 0) = Solved 1
divide ([], _) = Solved 0
divide (coins@(x:xs), n) | n < 0 = Solved 0
| otherwise = Pending (coins, n - x) (xs, n)
我希望前三种情况是显而易见的。最后一种情况是唯一具有多个子问题的情况。我们可以使用第一个列出的面额的一枚硬币,然后继续找较小金额的硬币,或者我们可以保持金额不变,但减少我们愿意使用的硬币面额列表。
组合子问题结果的代数要简单得多:我们只需将它们相加即可。
conquer :: Algebra ChangePuzzle Int
conquer (Solved n) = n
conquer (Pending a b) = a + b
我最初尝试写conquer = sum
(使用适当的 Foldable 实例),但这是不正确的。我们不是在总结a
输入子问题;相反,所有有趣的值都在 Solved 构造函数的 Int 字段中,并且sum
不看那些,因为它们不属于类型a
.
最后,我们让递归方案通过一个简单的方法为我们完成实际的递归hylo
call:
waysToMakeChange :: ChangePuzzleArgs -> Int
waysToMakeChange = hylo conquer divide
我们可以确认它在 GHCI 中有效:
*Main> waysToMakeChange (coins, 10)
4
*Main> waysToMakeChange (coins, 100)
292
您是否认为这值得付出努力取决于您。递归方案在这里为我们节省了很少的工作,因为这个问题很容易手工解决。但您可能会发现具体化中间状态使递归结构显式化,而不是隐式地隐含在调用图中。无论如何,如果您想练习递归方案以准备更复杂的任务,那么这是一个有趣的练习。
为了方便起见,完整的工作文件包含在下面。
{-# LANGUAGE DeriveFunctor #-}
import Control.Arrow ( (>>>), (<<<) )
newtype Term f = In {out :: f (Term f)}
type Algebra f a = f a -> a
type Coalgebra f a = a -> f a
cata :: (Functor f) => Algebra f a -> Term f -> a
cata fn = out >>> fmap (cata fn) >>> fn
ana :: (Functor f) => Coalgebra f a -> a -> Term f
ana f = In <<< fmap (ana f) <<< f
hylo :: Functor f => Algebra f b -> Coalgebra f a -> a -> b
hylo alg coalg = ana coalg >>> cata alg
data ChangePuzzle a = Solved Int
| Pending {spend, forget :: a}
deriving Functor
type Cent = Int
type ChangePuzzleArgs = ([Cent], Cent)
coins :: [Cent]
coins = [50, 25, 10, 5, 1]
divide :: Coalgebra ChangePuzzle ChangePuzzleArgs
divide (_, 0) = Solved 1
divide ([], _) = Solved 0
divide (coins@(x:xs), n) | n < 0 = Solved 0
| otherwise = Pending (coins, n - x) (xs, n)
conquer :: Algebra ChangePuzzle Int
conquer (Solved n) = n
conquer (Pending a b) = a + b
waysToMakeChange :: ChangePuzzleArgs -> Int
waysToMakeChange = hylo conquer divide