สถิติ: การรวมกันใน Python


122

ฉันจำเป็นต้องคำนวณ combinatorials (nCr) ในหลาม แต่ไม่สามารถหาฟังก์ชั่นที่จะทำในmath, numpyหรือstat ห้องสมุด สิ่งที่คล้ายกับฟังก์ชันประเภท:

comb = calculate_combinations(n, r)

ฉันต้องการจำนวนชุดค่าผสมที่เป็นไปได้ไม่ใช่ชุดค่าผสมที่แท้จริงดังนั้นitertools.combinationsฉันจึงไม่สนใจ

สุดท้ายนี้ฉันต้องการหลีกเลี่ยงการใช้แฟกทอเรียลเนื่องจากตัวเลขที่ฉันจะคำนวณชุดค่าผสมอาจมีขนาดใหญ่เกินไปและแฟกทอเรียลก็จะมหึมา

ดูเหมือนจะเป็นคำถามที่ตอบง่ายจริงๆ แต่ฉันกำลังจมอยู่กับคำถามเกี่ยวกับการสร้างชุดค่าผสมที่แท้จริงทั้งหมดซึ่งไม่ใช่สิ่งที่ฉันต้องการ

คำตอบ:


121

ดูscipy.special.comb (scipy.misc.comb ในเวอร์ชันเก่ากว่าของ scipy) เมื่อexactใดที่เป็นเท็จจะใช้ฟังก์ชัน gammaln เพื่อให้ได้ความแม่นยำที่ดีโดยไม่ต้องใช้เวลามาก ในกรณีที่แน่นอนจะส่งคืนจำนวนเต็มที่มีความแม่นยำโดยพลการซึ่งอาจใช้เวลาคำนวณนาน


5
scipy.misc.combจะเลิกในความโปรดปรานของรุ่นตั้งแต่scipy.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) )

ทดสอบ - พิมพ์สามเหลี่ยมของปาสคาล:

>>> 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 สำหรับการแนะนำให้เขียนอะไรง่ายๆสำหรับการใช้การลดและสำหรับการสาธิตที่ยอดเยี่ยมด้วยสามเหลี่ยมปาสคาล
jon_darkstar

6
-1 เพราะคำตอบนี้ผิด: พิมพ์แฟกทอเรียล (54) / (แฟกทอเรียล (54 - 27)) / แฟกทอเรียล (27) == nCk (54, 27) ให้ False
robert king

3
@robertking - โอเคคุณทั้งถูกต้องและถูกต้องทางเทคนิค สิ่งที่ฉันทำมีความหมายเพื่อเป็นภาพประกอบวิธีการเขียนหน้าที่ของตัวเอง ฉันรู้ว่ามันไม่แม่นยำสำหรับ N และ K ที่ใหญ่พอเนื่องจากความแม่นยำของจุดลอยตัว แต่เราสามารถแก้ไขได้ - ดูด้านบนตอนนี้ไม่ควรผิดพลาดสำหรับตัวเลขจำนวนมาก
Nas 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()เร็วกว่า 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

ทางออกที่ดีที่ไม่ต้องใช้ pkg ใด ๆ
Edward Newell

2
FYI: สูตรที่กล่าวถึงอยู่ที่นี่: en.wikipedia.org/wiki/…
jmiserez

chooseฟังก์ชั่นนี้น่าจะมีคะแนนโหวตมากกว่านี้! Python 3.8 มี math.comb แต่ฉันต้องใช้ Python 3.6 เพื่อความท้าทายและไม่มีการใช้งานใดให้ผลลัพธ์ที่แน่นอนสำหรับจำนวนเต็มขนาดใหญ่มาก อันนี้ทำได้และทำได้เร็ว!
สร้างใหม่

42

หากคุณต้องการผลที่แน่นอนและความเร็วลองgmpy - gmpy.combควรทำสิ่งที่คุณขอและก็สวยได้อย่างรวดเร็ว (แน่นอนเป็นgmpyของผู้เขียนต้นฉบับผมกำลังลำเอียง ;-)


6
แท้จริงแล้วgmpy2.comb()เป็น 10 ครั้งเร็วกว่าchoose()จากคำตอบของฉันรหัส: for k, n in itertools.combinations(range(1000), 2): f(n,k)ที่f()เป็นทั้งgmpy2.comb()หรือchoose()ในหลาม 3.
jfs

เนื่องจากคุณเป็นผู้เขียนแพคเกจฉันจะให้คุณแก้ไขลิงก์ที่ใช้งานไม่ได้เพื่อให้มันชี้ไปที่ที่ถูกต้อง ....
ไม่ค่อยมีความต้องการ

@SeldomNeedy เชื่อมโยงไปยัง code.google.com เป็นหนึ่งในสถานที่ที่เหมาะสม (แม้ว่าเว็บไซต์ที่อยู่ในโหมดการจัดเก็บในขณะนี้) แน่นอนจากนั้นมันง่ายที่จะหาสถานที่ GitHub, github.com/aleaxit/gmpyและ PyPI หนึ่งpypi.python.org/pypi/gmpy2ขณะที่มันเชื่อมโยงไปยังทั้งสอง -!)
อเล็กซ์เทล

@AlexMartelli ขออภัยในความสับสน หน้านี้จะแสดง 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)

ซึ่งเป็นจำนวนวิธีในการเลือก k รายการจาก n รายการโดยไม่ต้องทำซ้ำ
n! / (k! (n - k)!):

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

10

นี่เป็นทางเลือกอื่น เดิมเขียนด้วยภาษา C ++ ดังนั้นจึงสามารถย้อนกลับไปที่ C ++ สำหรับจำนวนเต็มที่มีความแม่นยำ จำกัด (เช่น __int64) ข้อดีคือ (1) เกี่ยวข้องกับการดำเนินการจำนวนเต็มเท่านั้นและ (2) หลีกเลี่ยงการขยายค่าจำนวนเต็มโดยการทำคู่การคูณและการหารต่อเนื่องกัน ฉันได้ทดสอบผลลัพธ์ด้วยรูปสามเหลี่ยม Pascal ของ Nas Banov มันได้รับคำตอบที่ถูกต้อง:

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 ฟังก์ชั่นที่เรียบง่ายที่จริงจะออกมาเป็นประมาณ 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

ทุกวันนี้มีมัณฑนากรบันทึกช่วยจำใน functools ชื่อ lru_cache ซึ่งอาจทำให้โค้ดของคุณง่ายขึ้น?
เม่นแคระ


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)

สั้นถูกต้องและมีประสิทธิภาพเพราะจะหลีกเลี่ยงจำนวนเต็มใหญ่ของหลามโดยยึดติดกับ longs

มีความแม่นยำและรวดเร็วกว่าเมื่อเปรียบเทียบกับ 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

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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.