Статистика: комбинации в Python

122

Мне нужно вычислить combinatorials (NCR) в Python , но не может найти функцию , чтобы сделать это в math, numpyили stat библиотеках. Что-то вроде функции типа:

comb = calculate_combinations(n, r)

Мне нужно количество возможных комбинаций, а не фактические комбинации, поэтому itertools.combinationsменя это не интересует.

Наконец, я хочу избежать использования факториалов, так как числа, для которых я буду вычислять комбинации, могут стать слишком большими, а факториалы будут чудовищными.

На этот вопрос кажется ДЕЙСТВИТЕЛЬНО легко ответить, однако меня тонут вопросы о генерации всех фактических комбинаций, чего я не хочу.

Morlock
источник

Ответы:

122

См. Scipy.special.comb (scipy.misc.comb в более старых версиях scipy). Если exactустановлено значение False, используется функция gammaln для получения хорошей точности, не занимая много времени. В точном случае он возвращает целое число произвольной точности, вычисление которого может занять много времени.

Йоуни К. Сеппянен
источник
5
scipy.misc.combустарел и заменен scipy.special.combверсией с 0.10.0.
Дилавар
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) )

Тест - печать треугольника Паскаля:

>>> 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 и за классную демонстрацию с треугольником
Паскаля
6
-1, потому что этот ответ неверен: print factorial (54) / (factorial (54-27)) / factorial (27) == nCk (54, 27) дает False.
Роберт
3
@robertking - Хорошо, вы были мелочными и технически корректными. То, что я сделал, было задумано как иллюстрация того, как написать собственную функцию; Я знал, что он не точен для достаточно больших N и K из-за точности с плавающей запятой. Но мы можем это исправить - см. Выше, теперь он не должен ошибаться для больших чисел
Нас Банов
9
Вероятно, это было бы быстро в Haskell, но, к сожалению, не в Python. На самом деле это довольно медленно по сравнению со многими другими ответами, например @Alex Martelli, JF Sebastian и моим собственным.
Тодд Оуэн,
9
Для Python 3 мне тоже пришлось from functools import reduce.
Велизар Христов
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()в 10 раз быстрее (проверено на всех парах 0 <= (n, k) <1e3), чем scipy.misc.comb()если вам нужен точный ответ.

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
JFS
источник
Хорошее решение, не требующее никаких пакетов
Эдвард Ньюэлл
2
К вашему сведению: указанная формула находится здесь: en.wikipedia.org/wiki/…
jmiserez
У этой chooseфункции должно быть больше голосов! В Python 3.8 есть math.comb, но мне пришлось использовать Python 3.6 для решения задачи, и ни одна реализация не дала точных результатов для очень больших целых чисел. Этот делает и делает это быстро!
реконструкция
42

Если вы хотите точные результаты и скорость, попробуйте gmpy - gmpy.combдолжны делать то , что вы просите, и это довольно быстро (конечно, как gmpy«s оригинальный автор, я имею в смещена ;-).

Алекс Мартелли
источник
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, ссылка на code.google.com - одно правильное место (хотя сайт сейчас находится в архивном режиме). Конечно, отсюда легко найти местоположение github, github.com/aleaxit/gmpy , и место PyPI, pypi.python.org/pypi/gmpy2 , поскольку оно связано с обоими! -)
Alex Martelli
@AlexMartelli Извините за путаницу. На странице отображается 404, если JavaScript был (выборочно) отключен. Полагаю, это для того, чтобы отговорить искусственного интеллекта-изгоев от того, чтобы так легко включать архивные исходники 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), это было более чем в 10 раз быстрее, чем один лайнер, reduceпредложенный в другом (в настоящее время наибольшее количество голосов) ответ. С другой стороны, он превосходит фрагмент, предоставленный @JF Sebastian.

Тодд Оуэн
источник
11

Начиная Python 3.8, стандартная библиотека теперь включает math.combфункцию для вычисления биномиального коэффициента:

math.comb (сущ., к)

что является количеством способов выбрать k элементов из n элементов без повторения
n! / (k! (n - k)!):

import math
math.comb(10, 5) # 252
Ксавье Гихот
источник
10

Вот еще одна альтернатива. Первоначально он был написан на C ++, поэтому его можно перенести на C ++ для целого числа конечной точности (например, __int64). Преимущество состоит в том, что (1) он включает только целочисленные операции и (2) позволяет избежать раздувания целочисленного значения путем последовательного выполнения пар умножения и деления. Я проверил результат с треугольником Паскаля Наса Банова, он дает правильный ответ:

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!

Чтобы максимально избежать переполнения при умножении, мы будем выполнять вычисления в следующем СТРОГОМ порядке слева направо:

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]
pantelis300
источник
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).

yzn-ФКУ
источник
4

Вы можете написать 2 простые функции, которые на самом деле примерно в 5-8 раз быстрее, чем при использовании scipy.special.comb . Фактически, вам не нужно импортировать какие-либо дополнительные пакеты, и функция довольно легко читается. Хитрость заключается в том, чтобы использовать мемоизацию для хранения ранее вычисленных значений и использовать определение 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
PyRsquared
источник
В наши дни в functools есть декоратор memoize под названием 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)))
MarianD
источник
3
я не думаю, что его временная сложность (и использование памяти) приемлемы.
xmcp
2

Прямая формула дает большие целые числа, когда n больше 20.

Итак, еще один ответ:

from math import factorial

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

короткий, точный и эффективный, потому что это позволяет избежать использования больших целых чисел Python за счет использования long.

Это точнее и быстрее по сравнению с 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
olivecoder
источник
Это не верно! Если n == r, результат должен быть 1. Этот код возвращает 0.
reyammer
Точнее, должно быть range(n-r+1, n+1)вместо range(n-r,n+1).
Reyammer
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

Это, вероятно, так быстро, как вы можете сделать это на чистом питоне для достаточно больших входных данных:

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
Сантьяго Кока Рохас
источник