Haskell中的尾部递归二项式系数函数

Haskell中的尾部递归二项式系数函数,haskell,recursion,tail-recursion,binomial-coefficients,Haskell,Recursion,Tail Recursion,Binomial Coefficients,我有一个计算Haskell中二项式系数的函数,它如下所示: binom :: Int -> Int -> Int binom n 0 = 1 binom 0 k = 0 binom n k = binom (n-1) (k-1) * n `div` k 是否可以修改它并使其尾部递归?可以。有一个标准的使用技巧。在您的情况下,您将需要其中两个(或累积一个有理数): 更新:对于较大的二项式系数,最好使用Integer,因为Int很容易溢出。此外,在上述简单的实现中,分子和分母都可以比最

我有一个计算Haskell中二项式系数的函数,它如下所示:

binom :: Int -> Int -> Int
binom n 0 = 1
binom 0 k = 0
binom n k = binom (n-1) (k-1) * n `div` k

是否可以修改它并使其尾部递归?

可以。有一个标准的使用技巧。在您的情况下,您将需要其中两个(或累积一个有理数):


更新:对于较大的二项式系数,最好使用
Integer
,因为
Int
很容易溢出。此外,在上述简单的实现中,分子和分母都可以比最终结果增长得更大。一个简单的解决方案是累加
Rational
,另一个方法是在每一步都将两者除以它们的值(AFAIK
Rational
在幕后就是这样做的)。

是的,如果引入一个带有额外参数的辅助函数:

-- calculate factor*(n choose k)
binom_and_multiply factor n 0 = factor
binom_and_multiply factor 0 k = 0
binom_and_multiply factor n k = binom (n-1) (k-1) (factor * n `div` k)

binom n k = binom_and_multiply 1 n k
最后一行可以用无点风格重写:

binom = binom_and_multiply 1

EDIT:上面的函数显示了这个想法,但实际上被破坏了,因为
div
操作数截断并与原始版本相反,没有数学证明要除法的值始终是分母的倍数。因此,这一职能必须由Petr Pudlák的建议取代:

-- calculate (n choose k) * num `div` denom
binom_and_multiply num denom _ 0 = num `div` denom
binom_and_multiply _   _     0 _ = 0
binom_and_multiply num denom n k = binom_and_multiply num denom (num * n) (denom * k) (n-1) (k-1)

binom = binom_and_multiply 1 1

在非优化haskell实现中,如果为
n
k
选择较高的值,您可能会对“正确尾部递归”变量仍然占用大量内存感到失望,因为您在非尾部递归实现中用堆空间交换堆栈空间,因为haskell太懒了,无法及时计算所有产品。它会一直等到您真正需要该值(可能需要打印它),然后在堆上存储两个产品表达式的表示形式。为了避免这种情况,您应该按照第一个和第二个参数所说的严格要求,使
binom_和_相乘
,这样在进行尾部递归时,产品将被急切地求值。例如,可以将
num
denom
比较为零,这将需要在继续之前评估因子的表达式:

-- calculate (n choose k) * num `div` denom
binom_and_multiply 0 0 _ _ = undefined  -- can't happen, div by zero
-- remaining expressions go here.
确保产品不会“蒸发到大”的一般方法是使用
seq
功能:

-- calculate (n choose k) * num `div` denom
binom_and_multiply num denom _ 0 = num `div` denom
binom_and_multiply _   _     0 _ = 0
binom_and_multiply num denom n k = 
      new_num = num*n
      new_denom = denom*k
    in new_num `seq` new_denom `seq` binom_and_multiply new_num new_denom (n-1) (k-1)
这告诉haskell实现,对
binom\u和\u multiply
的递归调用只能在对
new\u num
new\u denom
进行了求值之后(对WHNF,但解释WHNF超出了这个问题的范围)才会发生

最后一句话:这个答案通常被称为将右折叠转换为左折叠,然后将左折叠严格化。

使函数尾部递归的“自动”方法是使用(定义为尾部递归)重写它。 在Haskell中,一种简单易行的方法是将原始函数转换为一元形式,然后使用执行结果:

import Control.Monad.Cont

-- | Original function in monadic form
binomM n 0 = return 1
binomM 0 k = return 0
binomM n k = do
  b1 <- binomM (n-1) (k-1)
  return $! b1 * n `div` k

-- | Tail recursive mode of execution
binom :: Int -> Int -> Int
binom n k = binomM n k `runCont` id
import Control.Monad.Cont
--|一元形式的原始函数
binomM n 0=返回1
binomm0k=return0
binomM n k=do
b1整数->整数
binom n k=binomM n k`runCont`id
注意:通过这种方式,许多一元函数只需添加到它们的一元堆栈中,就可以转换为尾部递归函数

import Control.Monad.Cont

-- | Original function in monadic form
binomM n 0 = return 1
binomM 0 k = return 0
binomM n k = do
  b1 <- binomM (n-1) (k-1)
  return $! b1 * n `div` k

-- | Tail recursive mode of execution
binom :: Int -> Int -> Int
binom n k = binomM n k `runCont` id