Faktorieller Algorithmus effizienter als naive Multiplikation

38

Ich kann sowohl iterativ als auch rekursiv für Fakultäten codieren (zB n * factorial(n-1)für zB). Ich habe in einem Lehrbuch gelesen (ohne weitere Erklärungen erhalten zu haben), dass es eine noch effizientere Möglichkeit gibt, Fakultäten zu codieren, indem man sie rekursiv in zwei Hälften teilt.

Ich verstehe, warum das der Fall sein kann. Ich wollte es jedoch selbst versuchen und ich glaube nicht, wo ich anfangen soll. Ein Freund schlug vor, zuerst Basisfälle zu schreiben. und ich dachte daran, Arrays zu verwenden, damit ich die Zahlen verfolgen kann ... aber ich sehe wirklich keinen Ausweg, um einen solchen Code zu entwerfen.

Welche Techniken sollte ich erforschen?

user65165
quelle

Antworten:

40

Der beste Algorithmus, der bekannt ist, besteht darin, die Fakultät als Produkt der Primkräfte auszudrücken. Mit einem Siebansatz kann man schnell die Primzahlen sowie die richtige Leistung für jede Primzahl bestimmen. Die Berechnung jeder Potenz kann durch wiederholtes Quadrieren effizient durchgeführt werden, und dann werden die Faktoren miteinander multipliziert. Dies wurde von Peter B. Borwein, Über die Komplexität von Rechenfaktoren , Journal of Algorithms 6 376–380, 1985, beschrieben. ( PDF ) Kurz gesagt,kann in berechnet werden, verglichen mit der bei Verwendung der Definition erforderlichen -Zeit.n!O(n(logn)3loglogn)Ω(n2logn)

Was das Lehrbuch vielleicht bedeutete, war die Divide-and-Conquer-Methode. Man kann die Multiplikationen reduzieren, indem man das reguläre Muster des Produkts verwendet.n1

LassenBezeichnen Sie als geeignete Schreibweise. Ordne die Faktoren von als Nehmen wir nun an, dass für eine ganze Zahl . (Dies ist eine nützliche Annahme, um Komplikationen in der folgenden Diskussion zu vermeiden, und die Idee kann auf das allgemeine .) Dannund indem Sie diese Wiederholung erweitern, Rechnen1 3 5 ( 2 n - 1 ) ( 2 n ) ! = 1 2 3 ( 2 n ) ( 2 n ) ! = n ! 2 n3 5 7 ( 2 n - 1 ) . n = 2 k k >n?135(2n1)(2n)!=123(2n)

(2n)!=n!2n357(2n1).
n=2kn ( 2 k ) ! = ( 2 k - 1 ) ! 2 2 k - 1 ( 2 k - 1 ) ? ( 2 k ) ! = ( 2 2 k - 1 + 2 k - 2 + + 2 0 ) k - 1 i = 0 ( 2 i )k>0n(2k)!=(2k1)!22k1(2k1)?( 2 k & ndash ; 1 ) & dgr ; ( K - 2 ) + 2 k - 1 - 2 2 2 k - 2 2 2 k - 1
(2k)!=(22k1+2k2++20)i=0k1(2i)?=(22k1)i=1k1(2i)?.
(2k1)?und das Multiplizieren der Teilprodukte in jeder Stufe ergibt Multiplikationen. Dies ist eine Verbesserung um einen Faktor von fast gegenüber Multiplikationen unter Verwendung der Definition. Einige zusätzliche Operationen sind erforderlich, um die Potenz von zu berechnen , aber in der Binärarithmetik kann dies kostengünstig durchgeführt werden (je nachdem, was genau erforderlich ist, muss möglicherweise nur ein Suffix von Nullen hinzugefügt werden ).(k2)+2k1222k222k1

Der folgende Ruby-Code implementiert eine vereinfachte Version davon. Dies vermeidet nicht die Neuberechnung vonsogar wo es das könnte:n?

def oddprod(l,h)
  p = 1
  ml = (l%2>0) ? l : (l+1)
  mh = (h%2>0) ? h : (h-1)
  while ml <= mh do
    p = p * ml
    ml = ml + 2
  end
  p
end

def fact(k)
  f = 1
  for i in 1..k-1
    f *= oddprod(3, 2 ** (i + 1) - 1)
  end
  2 ** (2 ** k - 1) * f
end

print fact(15)

Auch dieser First-Pass-Code verbessert das Triviale

f = 1; (1..32768).map{ |i| f *= i }; print f

um etwa 20% in meinen Tests.

Mit ein wenig Arbeit kann dies weiter verbessert werden, und es entfällt auch die Anforderung, dass eine Potenz von (siehe die ausführliche Diskussion ).2n2

András Salamon
quelle
Sie haben einen wichtigen Faktor ausgelassen. Die Berechnungszeit nach Borweins Arbeit ist nicht O (n log n log log n). Es ist O (M (n log n) log log n), wobei M (n log n) die Zeit zum Multiplizieren von zwei Zahlen der Größe n log n ist.
gnasher729
18

Denken Sie daran, dass die Fakultätsfunktion so schnell wächst, dass Sie Ganzzahlen beliebiger Größe benötigen , um von effizienteren Techniken als dem naiven Ansatz zu profitieren. Die Fakultät von 21 ist bereits zu groß, um in ein 64-Bit-Format zu passen unsigned long long int.

Soweit ich weiß, gibt es keinen Algorithmus, um zu berechnen(Fakultät von ), was schneller ist als die Multiplikationen.¹nn!n

Die Reihenfolge, in der Sie die Multiplikationen durchführen, ist jedoch von Bedeutung. Die Multiplikation auf einer Maschinen-Ganzzahl ist eine grundlegende Operation, die unabhängig vom Wert der Ganzzahl dieselbe Zeit benötigt. Bei Ganzzahlen beliebiger Größe hängt die Zeit, die zum Multiplizieren von a und b benötigt wird, von der Größe von a und b ab : Ein naiver Algorithmus arbeitet in der Zeit (wobei ist Anzahl der Stellen von - in beliebiger Basis, da das Ergebnis bis auf eine multiplikative Konstante dasselbe ist). Es gibt schnellere Multiplikationsalgorithmen , aber es gibt eine offensichtliche Untergrenze von| x | x Ω ( | a | + | b | ) max ( | a | , | b | )Θ(|a||b|)|x|xΩ(|a|+|b|)da die multiplikation mindestens alle ziffern lesen muss. Alle bekannten Multiplikationsalgorithmen wachsen in schneller als linear .max(|a|,|b|)

Vor diesem Hintergrund sollte der Wikipedia-Artikel Sinn machen.

Da die Komplexität von Multiplikationen von der Größe der zu multiplizierenden Ganzzahlen abhängt, können Sie Zeit sparen, indem Sie Multiplikationen in einer Reihenfolge anordnen, in der die zu multiplizierenden Zahlen klein gehalten werden. Es funktioniert besser, wenn Sie dafür sorgen, dass die Zahlen ungefähr gleich groß sind. Die “Teilung in zwei Hälften”, auf die sich Ihr Lehrbuch bezieht, besteht aus dem folgenden Divide-and-Conquer- Ansatz zur Multiplikation einer (Multi-) Menge von ganzen Zahlen:

  1. Ordnen Sie die zu multiplizierenden Zahlen (anfangs alle ganzen Zahlen von bis ) in zwei Mengen an, deren Produkt ungefähr gleich groß ist. Dies ist wesentlich günstiger als die Multiplikation:(eine Maschinenergänzung).n | a b | | a | + | b |1n|ab||a|+|b|
  2. Wenden Sie den Algorithmus rekursiv auf jede der beiden Teilmengen an.
  3. Multiplizieren Sie die beiden Zwischenergebnisse.

Weitere Einzelheiten finden Sie im GMP-Handbuch .

Es gibt noch schnellere Methoden, die nicht nur die Faktoren bis neu ordnen , sondern die Zahlen auch aufteilen, indem sie in ihre Hauptfaktorisierung zerlegt und das resultierende sehr lange Produkt der meist kleinen ganzen Zahlen neu geordnet werden. Ich zitiere nur die Verweise aus dem Wikipedia-Artikel: „Über die Komplexität von Rechenfaktoren“ von Peter Borwein und Implementierungen von Peter Luschny .n1n

¹ Es gibt schnellere Möglichkeiten, Näherungen für berechnenAber das berechnet nicht mehr die Fakultät, sondern eine Annäherung.n!

Gilles 'SO - hör auf böse zu sein'
quelle
9

Da die Fakultätsfunktion so schnell wächst, kann Ihr Computer nur speichernfür relativ kleine . Zum Beispiel kann ein Double Werte bis zu speichern. Also, wenn Sie einen wirklich schnellen Algorithmus für die Berechnung vonVerwenden Sie einfach einen Tisch der Größe .n 171 ! n ! 171n!n171!n!171

Die Frage wird interessanter, wenn Sie an Oder an der Funktion (oder an ) interessiert sind . In all diesen Fällen (einschließlich ) Verstehe ich den Kommentar in Ihrem Lehrbuch nicht wirklich.Γ log Γ n !log(n!)ΓlogΓn!

Abgesehen davon sind Ihre iterativen und rekursiven Algorithmen äquivalent (bis zu Gleitkommafehlern), da Sie die Schwanzrekursion verwenden.

Yuval Filmus
quelle
"Ihre iterativen und rekursiven Algorithmen sind äquivalent" Sie beziehen sich auf die asymptotische Komplexität, oder? Was den Kommentar im Lehrbuch betrifft, so übersetze ich ihn aus einer anderen Sprache. Vielleicht ist meine Übersetzung scheiße.
user65165
Das Buch handelt von iterativ und rekursiv und kommentiert dann, wie Sie n durch Teilen und Erobern teilen können! In der Hälfte können Sie eine viel schnellere Lösung erhalten ...
user65165
1
Mein Äquivalenzbegriff ist nicht ganz formal, aber man könnte sagen, dass die durchgeführten arithmetischen Operationen gleich sind (wenn Sie die Reihenfolge der Operanden im rekursiven Algorithmus ändern). Ein "inhärent" anderer Algorithmus führt eine andere Berechnung durch, möglicherweise unter Verwendung eines "Tricks".
Yuval Filmus
1
Wenn Sie die Größe der Ganzzahl als Parameter für die Komplexität der Multiplikation betrachten, kann sich die Gesamtkomplexität ändern, auch wenn die arithmetischen Operationen "gleich" sind.
Tpecatte
1
@CharlesOkwuagwu Richtig, du könntest einen Tisch benutzen.
Yuval Filmus