统计信息:Python中的组合


122

我需要计算在Python combinatorials(NCR),但无法找到的功能做在mathnumpystat 图书馆。类似于函数的类型:

comb = calculate_combinations(n, r)

我需要可能的组合数量,而不是实际组合,因此itertools.combinations我对此并不感兴趣。

最后,我要避免使用阶乘,因为我将要计算其组合的数字可能会太大,并且阶乘将变得非常可怕。

这似乎是一个非常容易回答的问题,但是我被有关生成所有实际组合的问题淹没了,这不是我想要的。

Answers:


121

请参阅scipy.special.comb(旧版本的scipy中的scipy.misc.comb)。当exact为False时,它使用伽马函数来获得良好的精度而无需花费很多时间。在确切的情况下,它返回一个任意精度的整数,这可能需要很长时间才能计算出来。


5
scipy.misc.combscipy.special.comb从版本开始不推荐使用0.10.0
Dilawar

120

为什么不自己写呢?这是一线之类的:

from operator import mul    # or mul=lambda x,y:x*y
from fractions import Fraction

def nCk(n,k): 
  return int( reduce(mul, (Fraction(n-i, i+1) for i in range(k)), 1) )

测试-打印Pascal的三角形:

>>> for n in range(17):
...     print ' '.join('%5d'%nCk(n,k) for k in range(n+1)).center(100)
...     
                                                   1                                                
                                                1     1                                             
                                             1     2     1                                          
                                          1     3     3     1                                       
                                       1     4     6     4     1                                    
                                    1     5    10    10     5     1                                 
                                 1     6    15    20    15     6     1                              
                              1     7    21    35    35    21     7     1                           
                           1     8    28    56    70    56    28     8     1                        
                        1     9    36    84   126   126    84    36     9     1                     
                     1    10    45   120   210   252   210   120    45    10     1                  
                  1    11    55   165   330   462   462   330   165    55    11     1               
               1    12    66   220   495   792   924   792   495   220    66    12     1            
            1    13    78   286   715  1287  1716  1716  1287   715   286    78    13     1         
         1    14    91   364  1001  2002  3003  3432  3003  2002  1001   364    91    14     1      
      1    15   105   455  1365  3003  5005  6435  6435  5005  3003  1365   455   105    15     1   
    1    16   120   560  1820  4368  8008 11440 12870 11440  8008  4368  1820   560   120    16     1
>>> 

PS。编辑以替换int(round(reduce(mul, (float(n-i)/(i+1) for i in range(k)), 1)))int(reduce(mul, (Fraction(n-i, i+1) for i in range(k)), 1))因此对于大N / K不会出错


26
+1表示建议编写简单的内容,使用reduce以及带有Pascal三角形的酷炫演示
jon_darkstar

6
-1因为这个答案是错误的:print factorial(54)/(factorial(54-27))/ factorial(27)== nCk(54,27)给出False。
罗伯特·金

3
@robertking-好的,您既小巧又技术上正确。我所做的只是为了说明如何编写自己的功能。我知道由于浮点精度,对于足够大的N和K来说是不准确的。但是,我们可以解决这个问题-见上面,现在它不应该犯错的大数字
纳斯Banov

9
在Haskell中,这可能会很快,但是不幸的是,这不是Python。与许多其他答案(例如@Alex Martelli,JF Sebastian和我自己的答案)相比,它实际上相当慢。
Todd Owen

9
对于Python 3,我也必须from functools import reduce
Velizar Hristov

52

在Google代码上快速搜索给出了(它使用了@Mark Byers的答案中的公式):

def choose(n, k):
    """
    A fast way to calculate binomial coefficients by Andrew Dalke (contrib).
    """
    if 0 <= k <= n:
        ntok = 1
        ktok = 1
        for t in xrange(1, min(k, n - k) + 1):
            ntok *= n
            ktok *= t
            n -= 1
        return ntok // ktok
    else:
        return 0

choose()scipy.misc.comb()您需要确切答案快10倍(在所有0 <=(n,k)<1e3对上测试)。

def comb(N,k): # from scipy.comb(), but MODIFIED!
    if (k > N) or (N < 0) or (k < 0):
        return 0L
    N,k = map(long,(N,k))
    top = N
    val = 1L
    while (top > (N-k)):
        val *= top
        top -= 1
    n = 1L
    while (n < k+1L):
        val /= n
        n += 1
    return val

不需要任何pkg的好解决方案
Edward Newell,

2
仅供参考:提到的公式在这里:en.wikipedia.org/wiki/…–
jmiserez

这个choose功能应该有更多的投票权!Python 3.8具有math.comb,但是我不得不使用Python 3.6进行挑战,并且没有实现能够为非常大的整数提供准确的结果。这个做的很快。
reconn

42

如果您想要确切的结果速度,请尝试gmpygmpy.comb应该完全按照您的要求进行操作,而且速度非常快(当然,作为gmpy的原始作者,我偏见;-)。


6
事实上,gmpy2.comb()超过10倍的速度choose()从我的答案代码:for k, n in itertools.combinations(range(1000), 2): f(n,k)这里f()是要么gmpy2.comb()choose()Python的3
JFS

由于您是软件包的作者,所以我让修复断开的链接,使其指向正确的位置...
SeldomNeedy 2016年

@SeldomNeedy,指向code.google.com的链接是一个正确的位置(尽管该网站现在处于存档模式)。当然,从那里很容易找到github的位置github.com/aleaxit/gmpy和一个PyPI的pypi.python.org/pypi/gmpy2,因为它们都链接到这两个位置!-)
Alex Martelli

@AlexMartelli抱歉让您感到困惑。如果已(有选择地)禁用了javascript,则页面会显示404。我想这很容易阻止流氓AI轻松合并已归档的Google Code Project来源?
SeldomNeedy

28

如果您想要精确的结果,请使用sympy.binomial。看来这是最快的方法。

x = 1000000
y = 234050

%timeit scipy.misc.comb(x, y, exact=True)
1 loops, best of 3: 1min 27s per loop

%timeit gmpy.comb(x, y)
1 loops, best of 3: 1.97 s per loop

%timeit int(sympy.binomial(x, y))
100000 loops, best of 3: 5.06 µs per loop

22

在许多情况下,数学定义的字面翻译是足够的(记住Python将自动使用大数算法):

from math import factorial

def calculate_combinations(n, r):
    return factorial(n) // factorial(r) // factorial(n-r)

对于我测试的某些输入(例如n = 1000 r = 500),这比reduce另一种(目前投票率最高)答案中建议的一种衬板的速度快10倍以上。另一方面,@ JF Sebastian提供的代码片段的性能优于。


11

从开始Python 3.8,标准库现在包括math.comb用于计算二项式系数的函数:

math.comb(n,k)

这是从n个项中不重复选择k个项的方法的数量
n! / (k! (n - k)!)

import math
math.comb(10, 5) # 252

10

这是另一种选择。该代码最初是用C ++编写的,因此可以将其反向移植到C ++以获取有限精度的整数(例如__int64)。优点是(1)它仅涉及整数运算,(2)通过执行连续的乘法和除法对,避免了膨胀整数值。我已经用Nas Banov的Pascal三角形测试了结果,它得到了正确的答案:

def choose(n,r):
  """Computes n! / (r! (n-r)!) exactly. Returns a python long int."""
  assert n >= 0
  assert 0 <= r <= n

  c = 1L
  denom = 1
  for (num,denom) in zip(xrange(n,n-r,-1), xrange(1,r+1,1)):
    c = (c * num) // denom
  return c

基本原理:为了最小化乘法和除法的数量,我们将表达式重写为

    n!      n(n-1)...(n-r+1)
--------- = ----------------
 r!(n-r)!          r!

为了尽可能避免乘法溢出,我们将按照以下STRICT顺序从左到右进行评估:

n / 1 * (n-1) / 2 * (n-2) / 3 * ... * (n-r+1) / r

我们可以证明按此顺序运算的整数算术是精确的(即无舍入误差)。


5

使用动态编程,时间复杂度为Θ(n * m),空间复杂度为Θ(m):

def binomial(n, k):
""" (int, int) -> int

         | c(n-1, k-1) + c(n-1, k), if 0 < k < n
c(n,k) = | 1                      , if n = k
         | 1                      , if k = 0

Precondition: n > k

>>> binomial(9, 2)
36
"""

c = [0] * (n + 1)
c[0] = 1
for i in range(1, n + 1):
    c[i] = 1
    j = i - 1
    while j > 0:
        c[j] += c[j - 1]
        j -= 1

return c[k]

4

如果您的程序有上限n(例如n <= N),并且需要重复计算nCr(最好是>> N次),则使用lru_cache可以极大地提高性能:

from functools import lru_cache

@lru_cache(maxsize=None)
def nCr(n, r):
    return 1 if r == 0 or r == n else nCr(n - 1, r - 1) + nCr(n - 1, r)

构造缓存(隐式完成)需要花费O(N^2)时间。随后的所有对的调用都nCr将返回O(1)


4

您可以编写2个简单的函数,实际上比使用scipy.special.comb快5到8倍。实际上,您不需要导入任何额外的程序包,并且该函数非常易于阅读。诀窍是使用备忘录存储先前计算的值,并使用nCr的定义

# create a memoization dictionary
memo = {}
def factorial(n):
    """
    Calculate the factorial of an input using memoization
    :param n: int
    :rtype value: int
    """
    if n in [1,0]:
        return 1
    if n in memo:
        return memo[n]
    value = n*factorial(n-1)
    memo[n] = value
    return value

def ncr(n, k):
    """
    Choose k elements from a set of n elements - n must be larger than or equal to k
    :param n: int
    :param k: int
    :rtype: int
    """
    return factorial(n)/(factorial(k)*factorial(n-k))

如果我们比较时间

from scipy.special import comb
%timeit comb(100,48)
>>> 100000 loops, best of 3: 6.78 µs per loop

%timeit ncr(100,48)
>>> 1000000 loops, best of 3: 1.39 µs per loop

这些天来,在functools中有一个备忘录修饰器,称为lru_cache,它可以简化您的代码?
痴呆的刺猬

2

使用sympy很容易。

import sympy

comb = sympy.binomial(n, r)

2

仅使用随Python分发的标准库

import itertools

def nCk(n, k):
    return len(list(itertools.combinations(range(n), k)))

3
我不认为它的时间复杂度(和内存使用情况)是可以接受的。
xmcp

2

当n大于20时,直接公式会产生大整数。

因此,另一个回应是:

from math import factorial

reduce(long.__mul__, range(n-r+1, n+1), 1L) // factorial(r)

简短,准确和高效,因为它通过坚持使用long避免了python大整数。

与scipy.special.comb相比,它更准确,更快捷:

 >>> from scipy.special import comb
 >>> nCr = lambda n,r: reduce(long.__mul__, range(n-r+1, n+1), 1L) // factorial(r)
 >>> comb(128,20)
 1.1965669823265365e+23
 >>> nCr(128,20)
 119656698232656998274400L  # accurate, no loss
 >>> from timeit import timeit
 >>> timeit(lambda: comb(n,r))
 8.231969118118286
 >>> timeit(lambda: nCr(128, 20))
 3.885951042175293

错了!如果n == r,则结果应为1。此代码返回
0。– reyammer

更确切地说,应range(n-r+1, n+1)改为range(n-r,n+1)
reyammer '16

1

这是使用内置备忘录修饰器的@ killerT2333代码。

from functools import lru_cache

@lru_cache()
def factorial(n):
    """
    Calculate the factorial of an input using memoization
    :param n: int
    :rtype value: int
    """
    return 1 if n in (1, 0) else n * factorial(n-1)

@lru_cache()
def ncr(n, k):
    """
    Choose k elements from a set of n elements,
    n must be greater than or equal to k.
    :param n: int
    :param k: int
    :rtype: int
    """
    return factorial(n) / (factorial(k) * factorial(n - k))

print(ncr(6, 3))

1

这是为您提供的高效算法

for i = 1.....r

   p = p * ( n - i ) / i

print(p)

例如nCr(30,7)= fact(30)/(fact(7)* fact(23))=(30 * 29 * 28 * 27 * 26 * 25 * 24)/(1 * 2 * 3 * 4 * 5 * 6 * 7)

因此,只需从1到r运行循环即可获得结果。


0

对于相当大的输入,这可能与在纯python中完成的速度一样快:

def choose(n, k):
    if k == n: return 1
    if k > n: return 0
    d, q = max(k, n-k), min(k, n-k)
    num =  1
    for n in xrange(d+1, n+1): num *= n
    denom = 1
    for d in xrange(1, q+1): denom *= d
    return num / denom

0

此功能非常优化。

def nCk(n,k):
    m=0
    if k==0:
        m=1
    if k==1:
        m=n
    if k>=2:
        num,dem,op1,op2=1,1,k,n
        while(op1>=1):
            num*=op2
            dem*=op1
            op1-=1
            op2-=1
        m=num//dem
    return m
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.