【问题标题】:How is this hylo solution to the "coin-change" problem designed?这个解决“硬币找零”问题的 hylo 解决方案是如何设计的?
【发布时间】:2022-01-16 20:49:15
【问题描述】:

我在寻找 @amalloy 的 SO 上的 a nice post 时遇到了 hylomorhism 示例,这些示例通过有用的讨论和完整的实现来说明递归方案 (RS) 的使用:

{-# 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 Cent
                    | 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 Cent
conquer (Solved n) = n
conquer (Pending a b) = a + b

waysToMakeChange :: ChangePuzzleArgs -> Int
waysToMakeChange = hylo conquer divide

代码按预期工作。尽管对 RS 方面已经有了一些模糊的直觉,但我仍然想知道:

  1. 既然这是关于计数组合,为什么Solved Cent 而不是Solved Int? (如果这是一个合理的问题,这可能听起来像是一个小问题,但我希望它可能是下面其余不确定性的根源,尽管我怀疑我错过了更基本的东西!)。
  2. 因为我们稍后会求和,所以divideSolved 0/1 大概表示失败/成功?
  3. conquer 中,添加Pendingab 是什么意思?这 2 个值(如 Cents)表示什么,它们的总和在这种情况下意味着什么?
  4. conquer 中,我原以为我们只需要对Solveds 求和,作者谈到了这一点,但目前尚不清楚Pending 的情况如何起作用(例如修复@987654336 @确实对功能有不利影响,这可能是waysToMakeChange返回11的线索,或者该情况固定到的任何常量)。李>
  5. conquerab 中是Cents,而在divide 中它们是ChangePuzzleArgs(又名([Cent], Cent))——这种转换发生在哪里?

注意:作为 SO 新手,我无法在原始答案下方评论,这可能更合适,但我希望这也是有用的。

【问题讨论】:

    标签: haskell recursion functional-programming dynamic-programming recursion-schemes


    【解决方案1】:
    1. 既然这是关于计数组合,为什么Solved Cent 而不是Solved Int? (如果这是一个合理的问题,这可能听起来像是一个小问题,但我希望它可能是下面其余不确定性的根源,尽管我怀疑我错过了更基本的东西!)。

    我也会在这里使用Int

    1. 因为我们稍后在除法中求和,Solved 0/1 大概表示失败/成功?

    是的,但远不止于此。 Solved 0 的意思是“恰好有 0 种方法可以产生该变化量”(即失败),而 Solved 1 表示“恰好有 1 种方法可以产生该变化量”(即成功)。在后一种情况下,我们不仅指“成功”,而且还报告说只有一种方法可以解决任务。

    1. conquer 中,添加Pendingab 是什么意思?这 2 个值(如 Cents)表示什么,它们的总和在这种情况下意味着什么?

    本质上,Pending a ba,b::Int 的意思是“产生变化量的方法的数量可以分成两个不相交的集合,第一个具有a 元素,第二个具有b 元素” .

    当我们divide 时,我们返回Pending ... ... 以将问题拆分为两个不相交的子案例(coins, n - x)(xs, n)。这里coins=(x:xs)。我们根据是否要使用硬币x至少一次(因此我们需要用所有硬币生成n-x),或者我们根本不想使用它(因此我们需要生成@ 987654343@与其他硬币,仅限)。

    1. conquer 中,我原以为我们只需要对Solveds 求和,作者谈到了这一点,但目前尚不清楚待处理的案例是如何起作用的(例如修复conquer (Pending a b) = 11 确实对功能有不利影响,这可能是waysToMakeChange 返回 11 或该情况固定为任何常数的线索。

    总结所有Solved ... 是我们所做的。 cata 魔法本质上取代了简单的递归求和

    foo (Solved n) = n
    foo (Pending case1 case2) = foo case1 + foo case2
    

    cata conquer 在哪里

    conquer (Solved n) = n
    conquer (Pending a b) = a + b
    

    cata 的魔力使得在Pending 内部,我们找不到要递归的子树,而是已经计算出的递归结果。

    1. conquerab 中是Cents,而在divide 中它们是ChangePuzzleArgs(又名([Cent], Cent))——这种转换发生在哪里?

    一开始这确实很微妙。我只提供一个粗略的直觉。

    ana divide 之后,我们在函子ChangePuzzle 的一个不动点上产生一个结果。注意最后的ana 如何返回Term ChangePuzzle,这是固定点。在那里,([Cent], Cent) 一对神奇地消失了。

    当我们使用cata 时,Int 会再次出现,即使我们是从Term ChangePuzzle 开始的。

    非常粗略,你可以将Term ChangePuzzle 视为无限嵌套

    ChangePuzzle (ChangePuzzle (ChangePuzzle ( ....
    

    这与这样的树可能是任意嵌套的事实是一致的。在那里,ChangePuzzle 的“论据”基本上消失了。

    那么我们如何获得最终的Int?好吧,我们知道了,因为 Solved 总是采用 Int 参数,而不是 a 参数。这提供了使最终cata 递归工作的基本情况。

    【讨论】:

    • 同意。我在链接的答案中用 Int 替换了 Cent。
    • @chi - 感谢您的确认和解释!并以@amolloy 为例,让我思考这个问题,以及修复(不是双关语!)。我怀疑从CentInt(@Solvedconquer)的这些变化也有助于理解。所以,如果我有任何后续/相关的疑问,我会再看一遍,现在我可以写 cmets,在我选择接受的答案之前!
    猜你喜欢
    • 2015-09-02
    • 1970-01-01
    • 1970-01-01
    • 2015-03-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多