Haskell 为什么这个递归很慢?

Haskell 为什么这个递归很慢?,haskell,recursion,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)

我正在努力解决这个问题:

只有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)
结果是我的
递归
函数很慢,可能是二次时间或更糟。但我不明白为什么,因为它看起来很像斐波那契列表

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

递归的问题是,需要注意不要以指数形式进行分支,也不要有指数形式的内存足迹;此外,编写尾部递归函数通常表达性较差

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

或如第(1)条所述:

效率较低的替代方案是使用不可变的非严格(装箱)数组:

导入数据数组(listArray,(!) 计数::(数字a,可折叠的t)=>Int->Int->a 计数硬币总数=foldr go init硬币!全部的 哪里 init=listArray(0,总计)$1:重复0 出去 哪里 out=listary(0,总计)$map inc[0..total] inci=arr!我+如果我<硬币,那么其他0个出来!(i-硬币)

(1) 这个问题已经在stackoverflow的其他地方发布了;看

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

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

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

在第一种情况下,两个
foo
函数引用相同的对象,但在第二种情况下,
foo
的结果引用相同的对象。因此,在第一种情况下,如果
bar
想要引用它已经计算过的
fooa
的一部分,它必须重新计算整个过程。

只是猜测这与在递归的每一步使用take和drop有很大关系。这些是“O(a)”函数,也许尝试使用splitAt会是一个更好的选择?。另外,请记住,++也是一个“O(a)”操作,因为连接不是使用指针算法完成的,而是通过遍历整个结构完成的。我认为可能是这样,但后来我尝试了一个更简单的
recurseone xs=head xs:zipWith(+)(tail xs)(recurseone xs)
而且速度仍然很慢。您是否理解为什么您的代码一开始是正确的?通常,您可以从正确性证明(正确性属性的“终止”部分)推断ressource的使用情况(即复杂度界限)。在调用
递归
之间,您不会共享任何工作。如果您说
递归一个xs=let l=取一个xs++zipWith(+)(drop a xs)l在l
中,它更像
fibs
示例不说这一点会加快速度,因为在
foldr
中的
recurse
调用之间也没有共享工作。然而,您可以利用惰性来获得一个灵活的动态编程实现。@user3217013在惰性列表中,递归的有效性比良好的基础更重要。我的意思是,
让x=1:x在take100x
中终止,因为
1:x
是有效的。张贴的
fibs
使用相同的技巧。在OP代码中,我猜
采用xs++…
部分应该是使其高效的部分(如果
a
>0和
xs
不为空)。这看起来几乎与我的代码+Derek的编辑完全一样。。。唯一的区别是在前面加零,然后再加,而我使用
在前面加一个xs
。所以我想它也会有同样的表现。
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)
+------------+
v            v
foo a = bar (foo a)
foo a = r
          +-------+
          v       v
    where r = bar r