为什么这个递归缓慢?

时间:2016-04-18 16:41:47

标签: haskell recursion

我正在努力解决问题:

  

只使用1c,5c,10c,25c或50c硬币获得50美元的方式有多少种?

这是我的代码:

main = print $ coinCombinations [1,5,10,25,50] !! 5000

coinCombinations coins = foldr recurse (1 : repeat 0) coins
  where recurse a xs = take a xs ++ zipWith (+) (drop a xs) (recurse a xs)

事实证明我的recurse功能很慢,可能是二次或更差。但我不明白为什么,因为它看起来类似于斐波纳契列表

fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

2 个答案:

答案 0 :(得分:2)

递归的问题在于需要注意不要以指数方式分支或具有指数内存占用;并且编写尾递归函数通常不那么具有表现力。

您可以通过动态编程绕过整个递归开销;它在Haskell中使用正确的折叠具有非常高性能的实现:

count :: (Num a, Foldable t) => t Int -> Int -> a
count coins total = foldr go (1: repeat 0) coins !! total
    where
    go coin acc = out where out = zipWith (+) acc $ replicate coin 0 ++ out

然后:

\> count [1, 5, 10, 25, 50] 5000
432699251

或在31st problem of Project Euler (1)

\> count [1, 2, 5, 10, 20, 50, 100, 200] 200
73682

less 有效的替代方案是使用不可变的非严格(盒装)数组

import Data.Array (listArray, (!))

count :: (Num a, Foldable t) => t Int -> Int -> a
count coins total = foldr go init coins ! total
    where
    init = listArray (0, total) $ 1: repeat 0
    go coin arr = out
        where
        out = listArray (0, total) $ map inc [0..total]
        inc i = arr ! i + if i < coin then 0 else out ! (i - coin)

(1)问题已经发布在stackoverflow上的其他地方;见Using dynamic programming in Haskell? [Warning: ProjectEuler 31 solution inside]

答案 1 :(得分:0)

你是对的,这是二次时间。问题是

+------------+
v            v
foo a = bar (foo a)

不同
foo a = r
          +-------+
          v       v
    where r = bar r

在第一种情况下,两个foo函数引用同一个对象,但在第二种情况下,foo结果引用同一个对象。因此,在第一种情况下,如果bar想要引用它已经计算过的foo a的一部分,则必须再次计算整个事物。