【问题标题】:Why is this recursion slow?为什么这个递归很慢?
【发布时间】:2016-06-04 23:10:21
【问题描述】:

我正在尝试解决问题:

仅使用 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)

【问题讨论】:

  • 只是猜测这与您在递归的每一步使用 take 和 drop 有很大关系。这些是“O(a)”函数,也许尝试使用 splitAt 会是更好的选择?另外,请记住 ++ 也是一个“O(a)”操作,因为连接不是使用指针算术完成的,而是通过遍历整个结构来完成的。
  • 我以为可能是这样,但后来我尝试了一个更简单的recurseone xs = head xs : zipWith (+) (tail xs) (recurseone xs),它仍然很慢
  • 您首先了解为什么您的代码是正确的吗?通常,您可以从正确性证明(正确性属性的“终止”部分)推断资源使用情况(即复杂性界限)。
  • 您没有在对recurse 的调用之间共享任何工作。如果你说recurse a xs = let l = take a xs ++ zipWith (+) (drop a xs) l in l,它更像fibs 的例子。 '不是说这会很快,因为您也没有在foldr 中的recurse 的呼叫之间共享工作。但是,您可以利用惰性来获得动态编程的巧妙实现。
  • @user3217013 在惰性列表中,递归的有效性比有根据更重要。我的意思是,let x=1:x in take 100 x 将终止,因为1:x 是高效的。发布的fibs 使用了相同的技巧。在 OP 代码中,我猜想 take a xs ++ ... 部分应该使它富有成效(如果 a>0 和 xs 不为空)。

标签: haskell recursion


【解决方案1】:

递归的问题是需要注意不要以指数方式分支或具有指数内存足迹;并且写一个 tail recursive 函数通常表达力较差。

您可以通过动态编程绕过整个递归开销;它在 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

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

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]

【讨论】:

  • 这看起来几乎完全像我的代码 + Derek 的编辑......唯一的区别是你在前面添加零,然后添加,而我在前面使用 take a xs。所以我认为它会提供相同的性能。
【解决方案2】:

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

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

不一样
foo a = r
          +-------+
          v       v
    where r = bar r

在第一种情况下,两个foo函数引用同一个对象,但在第二种情况下,fooresult引用同一个对象。所以在第一种情况下,如果bar 想要引用它已经计算过的foo a 的一部分,它必须重新计算整个东西。

【讨论】:

    猜你喜欢
    • 2012-11-12
    • 1970-01-01
    • 1970-01-01
    • 2013-08-02
    • 2010-10-19
    • 2016-10-13
    • 1970-01-01
    • 1970-01-01
    • 2011-10-31
    相关资源
    最近更新 更多