2009-12-03 17 views
22

Cheers,calcular la cantidad de combinaciones

Sé que usted puede obtener la cantidad de combinaciones con la fórmula siguiente (sin repetición y el orden no es importante):

// Choose r from n 

n!/r!(n - r)!

Sin embargo, no lo creo saber cómo implementar esto en C++, ya que, por ejemplo, con

n = 52 

n! = 8,0658175170943878571660636856404e+67

obtiene el número demasiado grande incluso para unsigned __int64 (o unsigned long long). ¿Existe alguna solución para implementar la fórmula sin ninguna biblioteca "bigint" de terceros?

+0

para el uso grande int véase:
[http://stackoverflow.com/questions/1188939/representing-128-bit-numbers-in-c ] (http://stackoverflow.com/questions/1188939/representing-128-bit-numeros-in-c)
[http://stackoverflow.com/questions/1055661/bigint-bigbit-library](http: //stackoverflow.com/questions/1055661/bigint-bigbit-library)
[http://stackoverflow.com/questions/238343/big-number-in-c](http://stackoverflow. com/questions/238343/big-number-in-c)
SjB

Respuesta

33

Aquí es un antiguo algoritmo que es exacta y no se desborde a menos que el resultado es demasiado grande para un long long

unsigned long long 
choose(unsigned long long n, unsigned long long k) { 
    if (k > n) { 
     return 0; 
    } 
    unsigned long long r = 1; 
    for (unsigned long long d = 1; d <= k; ++d) { 
     r *= n--; 
     r /= d; 
    } 
    return r; 
} 

Este algoritmo está también en Knuth de "The Art of Computer Programming, tercera edición, volumen 2 : Algoritmos Seminumerical "Creo.

ACTUALIZACIÓN: Hay una pequeña posibilidad de que el algoritmo se desbordará en la línea:

r *= n--; 

para muy n grande. Un límite superior ingenuo es sqrt(std::numeric_limits<long long>::max()) lo que significa un n menos de 4,000,000,000.

+0

¿Podría esto mejorarse con 'r * = (n--)/d', para hacer la división primero? – GManNickG

+2

GManNickG, me parece que perderíamos precisión de esa manera. –

+0

Una pequeña explicación sobre cómo funciona esto habría sido excelente. –

5

Recuerde que

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

por lo que es la manera más pequeño que n !. Entonces, la solución es evaluar n * (n - 1) * ... * (n - r + 1) en lugar de primero calcular n! y luego dividiéndolo

Por supuesto, todo depende de la magnitud relativa de n y r - si r es relativamente grande en comparación con n, entonces todavía no cabe.

+0

Tenga en cuenta que la pregunta es cómo calcular n!/r! (n - r)! en lugar de n!/(n - r) !. – wenqiang

0

Simplifique primero la fórmula. No quieres hacer una división larga.

2

Bueno, tengo que responder a mi propia pregunta. Estaba leyendo sobre el triángulo de Pascal y por accidente se dio cuenta de que podemos calcular la cantidad de combinaciones con ella:

#include <iostream> 
#include <boost/cstdint.hpp> 

boost::uint64_t Combinations(unsigned int n, unsigned int r) 
{ 
    if (r > n) 
     return 0; 

    /** We can use Pascal's triange to determine the amount 
     * of combinations. To calculate a single line: 
     * 
     * v(r) = (n - r)/r 
     * 
     * Since the triangle is symmetrical, we only need to calculate 
     * until r -column. 
     */ 

    boost::uint64_t v = n--; 

    for (unsigned int i = 2; i < r + 1; ++i, --n) 
     v = v * n/i; 

    return v; 
} 

int main() 
{ 
    std::cout << Combinations(52, 5) << std::endl; 
}
+1

Sí, este es exactamente el mismo algoritmo que publiqué. Felicitaciones por idearlo usted mismo;) –

+0

Bueno, tengo mis momentos :) – nhaa123

+0

nota: desde C++ 11, 'uint64_t' es parte de' #include 'y por lo tanto ya no necesitamos usar' boost' para este ejemplo –

-1

Si quieres estar 100% seguro de que no rebosa ocurren siempre que el resultado final está dentro del numérico límite, se puede resumir fila por fila triángulo de Pascal:

for (int i=0; i<n; i++) { 
    for (int j=0; j<=i; j++) { 
     if (j == 0) current_row[j] = 1; 
     else current_row[j] = prev_row[j] + prev_row[j-1]; 
    } 
    prev_row = current_row; // assume they are vectors 
} 
// result is now in current_row[r-1] 

Sin embargo, este algoritmo es mucho más lento que el de la multiplicación. Entonces quizás podrías usar la multiplicación para generar todos los casos que sabes que son 'seguros' y luego usar la suma a partir de ahí. (... o podrías usar una biblioteca de BigInt).

+0

Como Andreas ha declarado en su respuesta, el desbordamiento podría ocurrir durante la multiplicación por 'n -'. No sucedería aquí. – int3

+0

Pero como ha indicado tendrá que esperar al final del universo para obtener la respuesta de este algoritmo;) –

+0

Esto no funciona para r = 0. Necesita modificar para devolver 1. –

22

De Andreas' answer:

Aquí está un antiguo algoritmo que es exacta y no se desborde a menos que el resultado es demasiado grande para un long long

unsigned long long 
choose(unsigned long long n, unsigned long long k) { 
    if (k > n) { 
     return 0; 
    } 
    unsigned long long r = 1; 
    for (unsigned long long d = 1; d <= k; ++d) { 
     r *= n--; 
     r /= d; 
    } 
    return r; 
} 

Este algoritmo está también en Knuth "El arte of Computer Programming, 3rd Edition, Volume 2: Algoritmos Seminumerical "Creo.

ACTUALIZACIÓN: Hay una pequeña posibilidad de que el algoritmo se desbordará en la línea:

r *= n--; 

para muy n grande. Un límite superior ingenuo es sqrt(std::numeric_limits<long long>::max()) lo que significa un n menos de 4,000,000,000.

Considere n == 67 yk == 33. El algoritmo anterior se desborda con un 64 bit sin signo de larga duración. Y, sin embargo, la respuesta correcta es representable en 64 bits: 14,226,520,737,620,288,370. Y el algoritmo anterior no dice nada acerca de su desbordamiento, seleccione (67, 33) devuelve:

8.829.174.638.479.413

Una respuesta creíble pero incorrecta.

Sin embargo, el algoritmo anterior se puede modificar ligeramente para que no se desborde nunca, siempre que la respuesta final sea representable.

El truco está en reconocer que en cada iteración, la división r/d es exacta. Temporalmente la reescritura:

r = r * n/d; 
--n; 

Para que esto sea exacta, que significa que si se expandió R, N y D en sus factores primos, entonces uno podría fácilmente anular d, y se quedará con un valor modificado para n, llamada que t, y luego el cálculo de r es simplemente:

// compute t from r, n and d 
r = r * t; 
--n; 

una manera rápida y fácil de hacer esto es encontrar el máximo común divisor de I + D, lo llaman g:

unsigned long long g = gcd(r, d); 
// now one can divide both r and d by g without truncation 
r /= g; 
unsigned long long d_temp = d/g; 
--n; 

Ahora podemos hacer la s ame cosa con d_temp yn (encuentre el divisor común más grande). Sin embargo, como sabemos a priori que r * n/d es exacto, también sabemos que gcd (d_temp, n) == d_temp, y por lo tanto no es necesario que lo calculemos. Así podemos dividir n por d_temp:

unsigned long long g = gcd(r, d); 
// now one can divide both r and d by g without truncation 
r /= g; 
unsigned long long d_temp = d/g; 
// now one can divide n by d/g without truncation 
unsigned long long t = n/d_temp; 
r = r * t; 
--n; 

Limpieza:

unsigned long long 
gcd(unsigned long long x, unsigned long long y) 
{ 
    while (y != 0) 
    { 
     unsigned long long t = x % y; 
     x = y; 
     y = t; 
    } 
    return x; 
} 

unsigned long long 
choose(unsigned long long n, unsigned long long k) 
{ 
    if (k > n) 
     throw std::invalid_argument("invalid argument in choose"); 
    unsigned long long r = 1; 
    for (unsigned long long d = 1; d <= k; ++d, --n) 
    { 
     unsigned long long g = gcd(r, d); 
     r /= g; 
     unsigned long long t = n/(d/g); 
     if (r > std::numeric_limits<unsigned long long>::max()/t) 
      throw std::overflow_error("overflow in choose"); 
     r *= t; 
    } 
    return r; 
} 

Ahora se puede calcular elegir (67, 33) sin rebosadero. Y si intentas elegir (68, 33), obtendrás una excepción en lugar de una respuesta incorrecta.

+0

Howard, I ' he arreglado el formato desordenado en tu respuesta. Lea las sugerencias de edición a la derecha del panel de edición para saber cómo hacerlo usted mismo. Ah, y bienvenido a SO! – sbi

+0

@sbi quería citar la respuesta aceptada, por lo que parecía un poco extraño. Para ser justos, el editor realmente apesta por algunas cosas imo. –

+0

@Johannes: ¡Oh, lo extrañé por completo! tal vez una pista sería apropiada? – sbi

4

La siguiente rutina calculará n-choose-k, usando la definición recursiva y la memorización.La rutina es extremadamente rápido y preciso:

inline unsigned long long n_choose_k(const unsigned long long& n, 
            const unsigned long long& k) 
{ 
    if (n < k) return 0; 
    if (0 == n) return 0; 
    if (0 == k) return 1; 
    if (n == k) return 1; 
    if (1 == k) return n;  
    typedef unsigned long long value_type; 
    value_type* table = new value_type[static_cast<std::size_t>(n * n)]; 
    std::fill_n(table,n * n,0); 
    class n_choose_k_impl 
    { 
    public: 

     n_choose_k_impl(value_type* table,const value_type& dimension) 
     : table_(table), 
     dimension_(dimension) 
     {} 

     inline value_type& lookup(const value_type& n, const value_type& k) 
     { 
     return table_[dimension_ * n + k]; 
     } 

     inline value_type compute(const value_type& n, const value_type& k) 
     { 
     if ((0 == k) || (k == n)) 
      return 1; 
     value_type v1 = lookup(n - 1,k - 1); 
     if (0 == v1) 
      v1 = lookup(n - 1,k - 1) = compute(n - 1,k - 1); 
     value_type v2 = lookup(n - 1,k); 
     if (0 == v2) 
      v2 = lookup(n - 1,k) = compute(n - 1,k); 
     return v1 + v2; 
     } 

     value_type* table_; 
     value_type dimension_; 
    }; 
    value_type result = n_choose_k_impl(table,n).compute(n,k); 
    delete [] table; 
    return result; 
} 
0

Obtener la descomposición en factores primos del coeficiente binomial es probablemente la forma más eficiente para calcularlo, especialmente si la multiplicación es caro. Esto es ciertamente cierto del problema relacionado de calcular factorial (ver Click here por ejemplo).

Aquí hay un algoritmo simple basado en el Tamiz de Eratóstenes que calcula la factorización prima. La idea es básicamente ir a través de los números primos cuando los encuentres usando el tamiz, pero luego también calcular cuántos de sus múltiplos caen en los rangos [1, k] y [n-k + 1, n]. El tamiz es esencialmente un algoritmo O (n \ log \ log n), pero no hay multiplicación. El número real de multiplicaciones necesarias una vez que se encuentra la factorización prima es, en el peor de los casos, O \ left (\ frac {n \ log \ log n} {\ log n} \ right) y probablemente haya formas más rápidas que eso.

prime_factors = [] 

n = 20 
k = 10 

composite = [True] * 2 + [False] * n 

for p in xrange(n + 1): 
if composite[p]: 
    continue 

q = p 
m = 1 
total_prime_power = 0 
prime_power = [0] * (n + 1) 

while True: 

    prime_power[q] = prime_power[m] + 1 
    r = q 

    if q <= k: 
     total_prime_power -= prime_power[q] 

    if q > n - k: 
     total_prime_power += prime_power[q] 

    m += 1 
    q += p 

    if q > n: 
     break 

    composite[q] = True 

prime_factors.append([p, total_prime_power]) 

print prime_factors 
0

Uno de camino más corto:

int nChoosek(int n, int k){ 
    if (k > n) return 0; 
    if (k == 0) return 1; 
    return nChoosek(n - 1, k) + nChoosek(n - 1, k - 1); 
} 
Cuestiones relacionadas