您将如何以最紧凑的方式为大型组合编写此算法?

时间:2009-06-05 15:33:04

标签: language-agnostic performance combinations binomial-coefficients

可从k项中检索的N项的组合数由以下公式描述。

             N! 
c =  ___________________ 
       (k! * (N - k)!)

一个例子是在彩票抽奖中可以从6 Balls的鼓中抽取多少48 Balls组合。

优化此公式以使用最小的O时间复杂度

运行

这个问题的灵感来自新的WolframAlpha数学引擎,以及它可以非常快速地计算出非常大的组合。例如以及随后在另一个论坛上讨论该主题。

  

http://www97.wolframalpha.com/input/?i=20000000+Choose+15000000

在一些人尝试解决方案之后,我会发布一些来自该讨论的信息/链接。

任何语言都可以接受。

7 个答案:

答案 0 :(得分:6)

Python: O (min [ k n - k ] 2

def choose(n,k):
    k = min(k,n-k)
    p = q = 1
    for i in xrange(k):
        p *= n - i
        q *= 1 + i
    return p/q

分析:

  • pq的大小会在循环内线性增加,如果可以认为n-i1+i具有不变的大小。
  • 每次乘法的成本也会线性增加。
  • 所有迭代的总和成为k上的算术系列。

我的结论: O k 2

如果重写为使用浮点数,则乘法将是原子操作,但是我们将失去很多精度。它甚至溢出choose(20000000, 15000000)。 (不是很大的惊喜,因为结果大约是0.2119620413×10 4884378 。)

def choose(n,k):
    k = min(k,n-k)
    result = 1.0
    for i in xrange(k):
        result *= 1.0 * (n - i) / (1 + i)
    return result

答案 1 :(得分:5)

请注意,WolframAlpha返回“十进制逼近”。如果您不需要绝对精度,则可以通过使用Stirling's Approximation计算阶乘来做同样的事情。

现在,斯特林的近似需要评估(n / e)^ n,其中e是自然对数的基础,这是迄今为止最慢的运算。但这可以使用another stackoverflow post中列出的技术来完成。

如果使用双精度和重复平方来完成求幂,则操作将为:

  • 斯特林近似的3次评估,每次评估需要O(log n)乘法和一个平方根评估。
  • 2次乘法
  • 1个部门

操作次数可能会有点聪明,但使用这种方法总时间复杂度将为O(log n)。非常易于管理。

编辑:考虑到这种计算的常见程度,关于这一主题的学术文献必然会有很多。一个好的大学图书馆可以帮助你追踪它。

EDIT2:另外,正如在另一个响应中指出的那样,这些值很容易溢出一个double,因此即使是适度大的k和n值,也需要使用具有非常高精度的浮点类型。

答案 2 :(得分:2)

我会在Mathematica中解决它:

Binomial[n, k]

男人,这很容易......

答案 3 :(得分:2)

Python 近似 O (1)?

使用python十进制实现来计算近似值。由于它不使用任何外部循环,并且数字的大小有限,我认为它将在 O (1)中执行。

from decimal import Decimal

ln = lambda z: z.ln()
exp = lambda z: z.exp()
sinh = lambda z: (exp(z) - exp(-z))/2
sqrt = lambda z: z.sqrt()

pi = Decimal('3.1415926535897932384626433832795')
e = Decimal('2.7182818284590452353602874713527')

# Stirling's approximation of the gamma-funciton.
# Simplification by Robert H. Windschitl.
# Source: http://en.wikipedia.org/wiki/Stirling%27s_approximation
gamma = lambda z: sqrt(2*pi/z) * (z/e*sqrt(z*sinh(1/z)+1/(810*z**6)))**z

def choose(n, k):
  n = Decimal(str(n))
  k = Decimal(str(k))
  return gamma(n+1)/gamma(k+1)/gamma(n-k+1)

示例:

>>> choose(20000000,15000000)
Decimal('2.087655025913799812289651991E+4884377')
>>> choose(130202807,65101404)
Decimal('1.867575060806365854276707374E+39194946')

任何更高,它会溢出。指数似乎限于40000000。

答案 4 :(得分:1)

给定n和K的合理数量的值,提前计算它们并使用查找表。

它以某种方式避开了这个问题(你正在卸载计算),但如果你必须确定大量的值,这是一个很有用的技术。

答案 5 :(得分:1)

<强> MATLAB:

  • 骗子的方式(使用内置函数NCHOOSEK): 13个字符,O(?)

    nchoosek(N,k)
    
  • 我的解决方案: 36个字符,O(分(k,N-k))

    a=min(k,N-k);
    prod(N-a+1:N)/prod(1:a)
    

答案 6 :(得分:1)

我知道这是一个非常古老的问题,但是我在这个问题的解决方案上挣扎了很长一段时间,直到我找到一个用VB 6编写的非常简单的解决方案并将其移植到C#之后,结果如下:

public int NChooseK(int n, int k)
{
    var result = 1;
    for (var i = 1; i <= k; i++)
    {
        result *= n - (k - i);
        result /= i;
    }

    return result;
}

最终的代码很简单,你不会相信它会运行直到你运行它。

此外,original article给出了他如何达到最终算法的一些很好的解释。