Why is this recursion slow?

孤人 提交于 2019-12-23 10:08:48

问题


I'm trying to solve the problem:

How many ways are there to get $50 using only 1c, 5c, 10c, 25c, or 50c coins?

Here's my code:

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)

It turns out that my recurse function is slow, maybe quadratic time or worse. But I don't understand why, since it looks similar to the fibonacci list

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

回答1:


The problem with recursion is that care needs to be taken not to branch exponentially or have exponential memory foot-print; and also writing a tail recursive function is usually less expressive.

You can bypass the entire recursion overhead by dynamic programming; which has a very performant implementation in Haskell using a right fold:

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

then:

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

or as in 31st problem of Project Euler (1):

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

A less efficient alternative would be to use immutable non-strict (boxed) arrays:

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) The problem is already posted elsewhere on stackoverflow; see Using dynamic programming in Haskell? [Warning: ProjectEuler 31 solution inside]




回答2:


You are correct, this is quadratic time. The problem is that

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

is not the same thing as

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

In the first case, the two foo functions refer to the same object, but in the second, the result of foo refers to the same object. So in the first case, if bar wants to refer to part of foo a it has already computed, it has to compute the whole thing again.



来源:https://stackoverflow.com/questions/36699695/why-is-this-recursion-slow

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!