阶乘算法比朴素乘法更有效


37

我知道如何使用迭代和递归(例如n * factorial(n-1),例如)为阶乘编码。我读过一本教科书(没有给出任何进一步的解释),发现通过递归将因式分解成两半,有一种更有效的编码方式。

我知道为什么会这样。但是我想尝试自己编码,但是我不知道从哪里开始。一位朋友建议我先写基本案例。我当时在考虑使用数组,以便可以跟踪数字...但是我真的看不出设计这种代码的任何出路。

我应该研究哪种技术?

Answers:


39

已知的最佳算法是将阶乘表示为素数乘积。可以使用筛子方法快速确定素数以及每个素数的合适功率。使用重复平方运算可以有效地计算每个功率,然后将这些系数相乘。这是由Peter B. Borwein描述,在复杂计算阶乘,算法杂志6 376-380,1985.(PDF)总之,可以用O (n (log n )3 log log n )时间来计算,与Ω (nn!O(n(log⁡n)3log⁡log⁡n)Ω(n2log⁡n)使用定义时需要 2 log n )时间。

教科书可能意味着分治法。通过使用乘积的规则模式,可以减少n−1乘法。

让分别表示1 ⋅ 3 ⋅ 5 ⋯ (2 Ñ - 1 )作为一个方便的符号。重新排列(2 n )的因数!= 1 ⋅ 2 ⋅ 3 ⋯ (2 Ñ )如 (2 Ñ )!= n !⋅ 2 Ñ ⋅ 3 ⋅ 5 ⋅ 7 ⋯ (2 Ñ -n?1⋅3⋅5⋯(2n−1)(2n)!=1⋅2⋅3⋯(2n) 现在假设对于某些整数 k > 0,n = 2 k。(这是一个有用的假设,可以避免在下面的讨论中引起麻烦,并且可以将其扩展到一般 n。)然后(2 k)!= (2 k − 1)!2 2 k - 1(2 k - 1)?并通过扩展此递归 (2 k)!=

(2n)!=n!⋅2n⋅3⋅5⋅7⋯(2n−1).
n=2kk>0n(2k)!=(2k−1)!22k−1(2k−1)? 计算( 2 k − 1)?
(2k)!=(22k−1+2k−2+⋯+20)∏i=0k−1(2i)?=(22k−1)∏i=1k−1(2i)?.
(2k−1)?并且在每个阶段中的部分积乘以花费乘法。仅使用定义,这就是从2 k - 2乘法中将近2倍的改进。需要一些附加的运算来计算2的幂,但是在二进制算术中,这可以廉价地完成(取决于所需的精确度,它可能只需要添加2 k - 1个零的后缀即可)。(k−2)+2k−1−222k−222k−1

n?

def oddprod(l,h)
  p = 1
  ml = (l%2>0) ? l : (l+1)
  mh = (h%2>0) ? h : (h-1)
  while ml <= mh do
    p = p * ml
    ml = ml + 2
  end
  p
end

def fact(k)
  f = 1
  for i in 1..k-1
    f *= oddprod(3, 2 ** (i + 1) - 1)
  end
  2 ** (2 ** k - 1) * f
end

print fact(15)

即使是此首过代码,其琐碎的改进

f = 1; (1..32768).map{ |i| f *= i }; print f

在我的测试中大约增加了20%

n2


您忽略了一个重要因素。根据Borwein的论文,计算时间不是O(n log n log log n)。它是O(M(n log n)log log n),其中M(n log n)是两个大小为n log n的数字相乘的时间。
— gnasher729

18

请记住,阶乘函数增长如此之快,以至于您需要任意大小的整数才能获得比朴素方法更有效的技术的任何好处。21的阶乘已经太大而无法容纳64位unsigned long long int。

n!n

Θ(|a|⋅|b|)|x|xΩ(|a|+|b|)max(|a|,|b|)

有了这样的背景,维基百科的文章应该是有道理的。

由于乘法的复杂性取决于要乘法的整数的大小,因此可以通过按顺序排列乘法以节省数字的时间来节省时间。如果您将数字安排为大致相同的大小,效果会更好。教科书所指的“二等分”由以下分治法相乘,以(整数)组整数相乘:

  1. 将要相乘的数字(最初是从到所有整数)安排在两组乘积中,乘积的大小大致相同。这比进行乘法要便宜得多:(一台机器)。n | 一个⋅ b | ≈ | 一个| + | b |1n|a⋅b|≈|a|+|b|
  2. 将算法递归应用于两个子集。
  3. 将两个中间结果相乘。

有关更多详细信息,请参见GMP手册。

甚至有更快的方法,不仅可以重新排列因子到还可以通过将数字分解为素数分解并重新排列所产生的非常小的整数的长乘积来拆分数字。我只是引用Wikipedia文章中的引用:Peter Borwein的“论计算阶乘的复杂性”和Peter Luschny的实现。n1n

¹ 有计算的更快的方法近似的,但这不再是在计算阶乘,而是在计算它的近似值。n!


9

由于阶乘函数增长如此之快,因此您的计算机只能存储对于相对较小的。例如,双精度型最多可以存储值。因此,如果您想要一种非常快速的计算算法,只需使用大小为的表。ñ 171 !n !171n!n171!n!171

如果您对或函数(或)感兴趣,这个问题将变得更加有趣。在所有这些情况下(包括),我都不太理解您教科书中的注释。Γ 日志Γ ñ !log⁡(n!)Γlog⁡Γn!

顺便说一句,您的迭代和递归算法是等效的(直到浮点错误),因为您使用的是尾递归。


“您的迭代算法和递归算法是等效的”,您指的是它们的渐近复杂性,对吗?至于教科书中的评论,那么我是从另一种语言翻译过来的,所以也许我的翻译很烂。
— user65165 2013年

本书讨论了迭代和递归,然后评论了如何使用分而治之来划分n!一半,您可以找到一种更快的解决方案...
— user65165

1
我的等效概念并不完全是形式上的,但是您可以说所执行的算术运算是相同的(如果您在递归算法中切换操作数的顺序)。一种“固有的”不同算法可能会使用一些“技巧”来执行不同的计算。
— Yuval Filmus 2013年

1
如果将整数的大小作为乘法复杂度的一个参数,即使算术运算“相同”,整体复杂度也会改变。
— Tpecatte

1
@CharlesOkwuagwu是的,您可以使用表格。
— Yuval Filmus
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.