2009-12-03 12 views
22

Cheers,Berechnung der Anzahl der Kombinationen

Ich weiß, dass Sie die Menge an Kombinationen der folgenden Formel erhalten können (ohne Wiederholung und Ordnung ist nicht wichtig):

// Choose r from n 

n!/r!(n - r)!

Allerdings weiß ich nicht wissen, wie dies in C++ implementieren, da zum Beispiel mit

n = 52 

n! = 8,0658175170943878571660636856404e+67

die Nummer bekommt viel zu groß, selbst für unsigned __int64 (oder unsigned long long). Gibt es einen Workaround, um die Formel ohne Bigint-Bibliotheken von Drittanbietern zu implementieren?

+0

für großes int siehe:
[http://stackoverflow.com/questions/1188939/representing-128-bit-numbers-in-c ] (http://stackoverflow.com/questions/1188939/representing-128-bit-numbers-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/Fragen/238343/big-number-in-c)
SjB

Antwort

33

Hier ist ein alter Algorithmus, der genau ist und nicht überläuft, wenn das Ergebnis für eine long long zu groß ist

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; 
} 

Dieser Algorithmus auch in Knuth ist „The Art of Computer Programming, 3. Auflage, Band 2 : Seminumerische Algorithmen "denke ich.

UPDATE: Es gibt eine kleine Möglichkeit, dass der Algorithmus auf der Linie überlaufen wird:

r *= n--; 

für sehr große n. Eine naive obere Grenze ist sqrt(std::numeric_limits<long long>::max()), was eine n weniger als ungefähr 4.000.000.000 bedeutet.

+0

Könnte dies durch 'r * = (n--)/d 'verbessert werden, um die Teilung zuerst zu tun? – GManNickG

+2

GManNickG, es scheint mir, dass wir so die Präzision verlieren würden. –

+0

Eine kleine Erklärung, wie das funktioniert wouldve groß gewesen .. –

5

Denken Sie daran, dass

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

so es ist viel kleiner als n !. Also lautet die Lösung, n * (n - 1) * ... * (n - r + 1) zu berechnen, anstatt zuerst n zu berechnen! und dann es teilen.

Natürlich hängt alles von der relativen Größe von n und r ab - wenn r im Vergleich zu n relativ groß ist, dann wird es immer noch nicht passen.

+0

Bitte beachten Sie, dass die Frage, wie n berechnen!/r! (n - r)! statt n!/(n - r) !. – wenqiang

0

Vereinfachen Sie zuerst die Formel. Du willst keine lange Trennung machen.

2

Nun, ich muss auf meine eigene Frage antworten. Ich lese über Pascals Dreieck und durch Zufall festgestellt, dass wir die Menge an Kombinationen mit ihm berechnen:

#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

Yup, das ist genau der gleiche Algorithmus wie ich gepostet habe. Kudos dafür, dass du es dir selbst ausgedacht hast;) –

+0

Nun, ich habe meine Momente :) – nhaa123

+0

Hinweis: Seit C++ 11 ist 'uint64_t' Teil von' #include 'und wir brauchen' Boost' daher nicht mehr zu verwenden Dieses Beispiel –

-1

Wenn Sie 100% sicher sein wollen, dass keine Überläufe so lange auftreten, da das Endergebnis in der numerisch

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] 

jedoch dieser Algorithmus ist viel langsamer als die Multiplikation ein: Limit, können Sie Pascals Dreieck Zeile-für-Zeile zusammenzufassen. Vielleicht könnten Sie die Multiplikation verwenden, um alle Fälle zu generieren, von denen Sie wissen, dass sie "sicher" sind, und dann die Addition von dort verwenden. (.. oder Sie könnten einfach eine BigInt-Bibliothek verwenden).

+0

Wie Andreas in seiner Antwort erklärt hat, konnte es bei der Multiplikation mit 'n -' zu einem Überlauf kommen. Es würde hier nicht passieren. – int3

+0

Aber wie Sie gesagt haben, müssten Sie warten auf das Ende des Universums für die Antwort von diesem Algorithmus;) –

+0

Dies funktioniert nicht für r = 0. Muss geändert werden, um zurückzukehren 1. –

22

Von Andreas' answer:

Hier ist ein alter Algorithmus, der genau ist und nicht überläuft, wenn das Ergebnis für eine long long zu groß ist

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; 
} 

Dieser Algorithmus auch in Knuth ist „The Art der Computerprogrammierung, 3. Auflage, Band 2: Seminumerische Algorithmen "Ich denke.

UPDATE: Es gibt eine kleine Möglichkeit, dass der Algorithmus auf der Linie überlaufen wird:

r *= n--; 

für sehr große n. Eine naive obere Grenze ist sqrt(std::numeric_limits<long long>::max()), was eine n weniger als ungefähr 4.000.000.000 bedeutet.

Betrachten Sie n == 67 und k == 33. Der obige Algorithmus überläuft mit einem 64 Bit unsigned long long. Und dennoch ist die richtige Antwort in 64 Bits darstellbar: 14,226,520,737,620,288,370. Und der obige Algorithmus ist um seine Überlauf still, wählen (67, 33) zurückgibt:

8.829.174.638.479.413

Eine glaubwürdige, aber falsche Antwort.

Der obige Algorithmus kann jedoch leicht modifiziert werden, um niemals zu überlaufen, solange die endgültige Antwort darstellbar ist.

Der Trick besteht darin zu erkennen, dass bei jeder Iteration die Division r/d genau ist. Vorübergehend Umschreiben:

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

Für diese genau zu sein, bedeutet es, wenn Sie r erweitert, n und d in ihre Primfaktorzerlegung, dann könnte man leicht d zunichte machen und mit einem modifizierten Wert für n gelassen werden, rufen es t, und dann ist die Berechnung von r einfach:

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

Eine schnelle und einfache Möglichkeit, dies zu tun, ist der größte gemeinsame Teiler von r und d, rufen sie finden 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; 

Jetzt können wir die s machen ame Sache mit d_temp und n (Finde den größten gemeinsamen Teiler). Da wir a-priori wissen, dass r * n/d genau ist, wissen wir auch, dass gcd (d_temp, n) == d_temp, und daher müssen wir es nicht berechnen. So können wir n durch d_temp teilen:

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; 

Reinigung:

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; 
} 

Jetzt können Sie berechnen, wählen (67, 33) ohne Überlauf. Und wenn Sie versuchen, wählen Sie (68, 33), erhalten Sie eine Ausnahme anstelle einer falschen Antwort.

+0

Howard, ich ' Die fehlerhafte Formatierung in Ihrer Antwort wurde korrigiert. Bitte lesen Sie die Bearbeitungshinweise rechts neben dem Bearbeitungsfenster, um zu erfahren, wie Sie dies selbst tun können. Oh, und sehr willkommen in SO! – sbi

+0

@sbi wollte er die angenommene Antwort zitieren, weshalb es ein bisschen komisch aussah. In aller Fairness ist der Editor wirklich ein paar Dinge imo. –

+0

@Johannes: Oh, das habe ich total vermisst! Vielleicht wäre ein Hinweis angebracht? – sbi

4

Die folgende Routine berechnet das n-choose-k unter Verwendung der rekursiven Definition und Memoisierung.Die Routine ist extrem schnell und präzise:

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

gibt es die Primfaktorzerlegung des Binomialkoeffizient ist wahrscheinlich die effizienteste Art und Weise zu berechnen, vor allem, wenn Multiplikation teuer ist. Dies trifft sicherlich auf das verwandte Problem der Berechnung der Fakultät zu (siehe beispielsweise Click here).

Hier ist ein einfacher Algorithmus auf dem Sieb des Eratosthenes aus, dass die Primfaktorzerlegung berechnet. Die Idee ist im Grunde durch die Primzahlen zu gehen, wie Sie sie mit dem Sieb zu finden, aber dann auch zu berechnen, wie viele ihrer Multiples in den Bereichen fallen [1, k] und [n-k + 1, n]. Das Sieve ist im Wesentlichen ein O (n \ log \ log n) Algorithmus, aber es gibt keine Multiplikation. Die tatsächliche Anzahl von Multiplikationen, die notwendig sind, wenn die Primfaktor-Faktorisierung gefunden wird, ist schlimmstenfalls schlecht (\ frac {n \ log \ log n} {\ log n} \ right) und es gibt wahrscheinlich schnellere Wege als das.

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

Ein kürzesten Weg:

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); 
} 
Verwandte Themen