Haskell:如何记忆这个算法?

时间:2018-05-13 07:36:15

标签: algorithm performance haskell dynamic-programming memoization

我将此解决方案写入coin change problem on HackerRank

makeChange :: Int -> [Int] -> Int
makeChange n ys = go n (sort ys)
    where go _ [] = 0
          go n (x:xs)
            | x == n = 1
            | x > n = 0
            | otherwise = (makeChange n xs) + (makeChange (n - x) (x:xs))

然而,它在一些较大的测试用例上超时。我看到了this关于使用let绑定实现memoization的文章,但它主要是我的头脑,我不知道如何在这里实现它。有什么帮助吗?

我重新编写了它并获得了显着的性能提升,但我仍然在进行黑客排名练习:

makeChange' :: Int -> [Int] -> Int
makeChange' =
    let go _ [] = 0
        go n (x:xs)
          | x == n = 1
          | x > n = 0
          | otherwise = (makeChange' n xs) + (makeChange' (n - x) (x:xs))
    in go

f (x:y:xs) = makeChange' x ys
    where ys = sort xs
main = interact $ show . f . map read . words

我将预排序移动到中间函数f,我也用它来处理来自Hacker Rank的输入。它们为您提供了一个字符串,其中包含更改目标,更改数组的长度以及更改单元数组。我使用f从输入中删除长度。

1 个答案:

答案 0 :(得分:2)

我将尝试演示behzad.nouri的代码是如何工作的,以便自己理解它:

记住Haskell的懒惰,我们发现!! n意味着我们的最终列表将被评估为(n+1)元素。我们对每枚硬币的主要区块是:

let b = (take x a) ++ zipWith (+) b (drop x a) in b

这里有一个小技巧,因为b是一个列表。请注意,如果b是整数,那么类似的自引用将不起作用。让我们说我们唯一的硬币是2:

 > let b = (take 2 [1,0,0,0,0]) ++ zipWith (+) b (drop 2 [1,0,0,0,0]) in b
=> [1,0,1,0,1]

(这意味着一种方法可以使零,一种方法可以产生两种,一种方式可以产生四种方式。)b以递归方式构建,因为它是自我引用的,直到有一个方法。拉链评估的长度匹配:

 > zipWith (+) [] (drop 2 [1,0,0,0,0])
=> []
-- But we prepended [1,0] so the next `b` is [1,0]
 > zipWith (+) [1,0] (drop 2 [1,0,0,0,0])
=> [1,0]
-- But we prepended [1,0] so the next `b` is [1,0,1,0]
 > zipWith (+) [1,0,1,0] (drop 2 [1,0,0,0,0])
=> [1,0,1]
-- But we prepended [1,0] so the result matching the length is [1,0,1,0,1]

现在让我们使用这些知识来完成solve 4 [1,2,3]

let b = (take 3 [1,0,0,0,0]) ++ zipWith (+) b (drop 3 [1,0,0,0,0]) in b

take 3 [1,0,0,0,0] -- n+1 elements are evaluated from the infinite list
=> [1,0,0]

++       [0,0] -- drop 3 from [1,0,0,0,0]
     (+) []
     =>  []
     -- prepend [1,0,0]
     =>  [1,0,0]

++       [0,0]
     (+) [1,0,0]
     =>  [1,0]
     -- prepend [1,0,0]
     =>  [1,0,0,1,0]

这是制造零的一种方式,也是制造三种方式的一种方式。下一个:

let b = (take 2 [1,0,0,1,0]) ++ zipWith (+) b (drop 2 [1,0,0,1,0]) in b

take 2 [1,0,0,1,0]
=> [1,0]

++     [0,1,0]
   (+) []
   =>  []
   -- prepend [1,0]
   =>  [1,0]

++     [0,1,0]
   (+) [1,0]
   =>  [1,1,0]
   -- prepend [1,0]
   =>  [1,0,1,1,0]

++     [0,1,0]
   (+) [1,0,1,1,0]
   =>  [1,1,1]
   -- prepend [1,0]
   =>  [1,0,1,1,1]

这是制作0的一种方法,一种制作2的方法,一种制作3的方法,以及一种制作方法4.下一步:

let b = (take 1 [1,0,1,1,1]) ++ zipWith (+) b (drop 1 [1,0,1,1,1]) in b

这是迭代次数最多的一次,因为b仅由一个元素构建

take 1 [1,0,1,1,1]
=> [1]

++   [0,1,1,1]
 (+) []
 =>  []
 -- prepend [1]
 =>  [1]

++   [0,1,1,1]
 (+) [1]
 =>  [1]
 -- prepend [1]
 =>  [1,1]

++   [0,1,1,1]
 (+) [1,1]
 =>  [1,2]
 -- prepend [1]
 =>  [1,1,2]

++   [0,1,1,1]
 (+) [1,1,2]
 =>  [1,2,3]
 -- prepend [1]
 =>  [1,1,2,3]

++   [0,1,1,1]
 (+) [1,1,2,3]
 =>  [1,2,3,4]
 -- prepend [1]
 =>  [1,1,2,3,4]

这是制作0的1种方法,1种制作1种方法,2种制作2种方法,3种方式制作3种方法以及4种方式制作4种方式。