Diese Herausforderung ist teilweise eine Algorithmus-Herausforderung, beinhaltet etwas Mathematik und ist teilweise einfach eine schnellste Code-Herausforderung.
n
Betrachten Sie für eine positive ganze Zahl eine gleichmäßig zufällige Zeichenfolge mit 1
s und 0
s Länge n
und nennen Sie sie A
. Betrachten sie nun auch einen zweiten einheitlich gewählt zufälligen Zeichenfolge der Länge , n
deren Werte -1
, 0,
oder 1
es nennen B_pre
. Nun wollen wir B
sein B_pre
+ B_pre
. Das ist B_pre
mit sich selbst verknüpft.
Betrachten Sie nun das innere Produkt von A
und B[j,...,j+n-1]
und nennen Sie es Z_j
und index from 1
.
Aufgabe
Die Ausgabe sollte eine Liste von n+1
Brüchen sein. Der i
dritte Term in der Ausgabe sollte die genaue Wahrscheinlichkeit sein, dass alle ersten i
Terms Z_j
mitj <= i
gleich 0
.
Ergebnis
Das größte n
für die Ihr Code auf meinem Computer in weniger als 10 Minuten die richtige Ausgabe liefert.
Kabelbinder
Wenn zwei Antworten die gleiche Punktzahl haben, gewinnt die zuerst eingereichte.
In dem (sehr sehr) unwahrscheinlichen Fall, dass jemand eine Methode findet, um unbegrenzte Punktzahlen zu erhalten, wird der erste gültige Beweis für eine solche Lösung akzeptiert.
Hinweis
Versuchen Sie nicht, dieses Problem mathematisch zu lösen, es ist zu schwer. Meiner Ansicht nach ist es der beste Weg, zu den grundlegenden Definitionen der Wahrscheinlichkeit von der High School zurückzukehren und kluge Wege zu finden, um den Code dazu zu bringen, eine erschöpfende Aufzählung der Möglichkeiten vorzunehmen.
Sprachen und Bibliotheken
Sie können jede Sprache verwenden, die über einen frei verfügbaren Compiler / Interpreter / etc. Verfügt. für Linux und alle Bibliotheken, die auch für Linux frei verfügbar sind.
Meine Maschine Die Timings werden auf meinem Computer ausgeführt. Dies ist eine Ubuntu-Standardinstallation auf einem AMD FX-8350 Eight-Core-Prozessor. Dies bedeutet auch, dass ich in der Lage sein muss, Ihren Code auszuführen. Verwenden Sie daher nur leicht verfügbare kostenlose Software und fügen Sie vollständige Anweisungen zum Kompilieren und Ausführen Ihres Codes bei.
Einige Testausgaben. Betrachten Sie jeweils nur die erste Ausgabe n
. Dann ist es soweit i=1
. Für n
1 bis 13 sollten sie sein.
1: 4/6
2: 18/36
3: 88/216
4: 454/1296
5: 2424/7776
6: 13236/46656
7: 73392/279936
8: 411462/1679616
9: 2325976/10077696
10: 13233628/60466176
11: 75682512/362797056
12: 434662684/2176782336
13: 2505229744/13060694016
Eine allgemeine Formel für finden Sie auch i=1
unter http://oeis.org/A081671 .
Leaderboard (aufgeschlüsselt nach Sprache)
- n = 15. Python + Parallelpython + Pypy in 1min49s von Jakube
- n = 17. C ++ in 3min37s von Keith Randall
- n = 16. C ++ in 2min38s von kuroi neko
quelle
Antworten:
C ++, n = 18 in 9 min auf 8 Threads
(Lassen Sie mich wissen, ob es auf Ihrem Computer weniger als 10 Minuten dauert.)
Ich nutze verschiedene Formen der Symmetrie im B-Array. Diese sind zyklisch (Verschiebung um eine Position), umgekehrt (Reihenfolge der Elemente umkehren) und vorzeichenbehaftet (Negativ jedes Elements nehmen). Zuerst berechne ich die Liste der Bs, die wir versuchen müssen, und ihr Gewicht. Dann durchläuft jedes B eine schnelle Routine (unter Verwendung von Bitcount-Befehlen) für alle 2 ^ n-Werte von A.
Hier ist das Ergebnis für n == 18:
Kompilieren Sie das folgende Programm mit
g++ --std=c++11 -O3 -mpopcnt dot.cc
quelle
-pthread
wieder erinnern . Ich kommen=17
auf meine Maschine.Python 2 mit pypy und pp: n = 15 in 3 Minuten
Auch nur eine einfache rohe Kraft. Interessant zu sehen, dass ich mit C ++ fast die gleiche Geschwindigkeit wie kuroi neko bekomme. Mein Code kann erreichen
n = 12
in ca. 5 Minuten erreicht werden. Und ich führe es nur auf einem virtuellen Kern aus.Bearbeiten: Reduziert den Suchraum um den Faktor
n
Mir ist aufgefallen, dass ein zyklisierter Vektor
A*
vonA
dieselben Zahlen wie Wahrscheinlichkeiten (dieselben Zahlen) wie der ursprüngliche Vektor erzeugt,A
wenn ich darüber iteriereB
. Eg Der Vektor(1, 1, 0, 1, 0, 0)
hat die gleichen Wahrscheinlichkeiten als jeder der Vektoren(1, 0, 1, 0, 0, 1)
,(0, 1, 0, 0, 1, 1)
,(1, 0, 0, 1, 1, 0)
,(0, 0, 1, 1, 0, 1)
und ,(0, 1, 1, 0, 1, 0)
wenn eine zufällige AuswahlB
. Deshalb muss ich nicht jede dieser sechs Vektoren durchlaufen, aber nur etwa 1 und ersetzencount[i] += 1
mitcount[i] += cycle_number
.Dies reduziert die Komplexität von
Theta(n) = 6^n
zuTheta(n) = 6^n / n
. Deshalb istn = 13
es ungefähr 13 mal so schnell wie meine Vorgängerversion. Es rechnetn = 13
in ca. 2 Minuten 20 Sekunden. Dafür istn = 14
es noch etwas zu langsam. Es dauert ungefähr 13 Minuten.edit 2: Multi-Core-Programmierung
Mit der nächsten Verbesserung nicht wirklich zufrieden. Ich habe mich entschlossen, mein Programm auch auf mehreren Kernen auszuführen. Auf meinen 2 + 2 Kernen kann ich jetzt
n = 14
in ca. 7 Minuten rechnen . Nur ein Faktor 2 Verbesserung.Der Code ist in diesem Github-Repo verfügbar: Link . Die Mehrkernprogrammierung macht das etwas hässlich.
Bearbeiten 3: Reduzieren des Suchraums für
A
Vektoren undB
VektorenIch bemerkte die gleiche Spiegelsymmetrie für die Vektoren
A
wie kuroi neko. Immer noch nicht sicher, warum das funktioniert (und ob es für jeden funktioniert)n
).Die Reduzierung des Suchraums für
B
Vektoren ist etwas cleverer. Ich habe die Erzeugung der Vektoren (itertools.product
) durch eine eigene Funktion ersetzt. Grundsätzlich beginne ich mit einer leeren Liste und lege sie auf einen Stapel. Bis der Stapel leer ist, entferne ich eine Liste. Wenn sie nicht dieselbe Länge hat wien
, erstelle ich 3 weitere Listen (indem ich -1, 0, 1 anhänge) und schiebe sie auf den Stapel. Wenn eine Liste die gleiche Länge hat wien
, kann ich die Summen auswerten.Jetzt, da ich die Vektoren selbst generiere, kann ich sie filtern, je nachdem, ob ich die Summe = 0 erreichen kann oder nicht. Wenn z. B. mein Vektor
A
ist(1, 1, 1, 0, 0)
und mein VektorB
aussieht(1, 1, ?, ?, ?)
, weiß ich, dass ich die nicht?
mit Werten füllen kann , so dassA*B = 0
. Ich muss also nicht alle 6 VektorenB
des Formulars durchlaufen(1, 1, ?, ?, ?)
.Wir können dies verbessern, wenn wir die Werte für 1 ignorieren. Wie in der Frage erwähnt, sind die Werte für
i = 1
die Sequenz A081671 . Es gibt viele Möglichkeiten, diese zu berechnen. Ich wähle die einfache Wiederholung:a(n) = (4*(2*n-1)*a(n-1) - 12*(n-1)*a(n-2)) / n
. Da wiri = 1
im Grunde genommen in kürzester Zeit rechnen können, können wir weitere Vektoren nach filternB
. ZBA = (0, 1, 0, 1, 1)
undB = (1, -1, ?, ?, ?)
. Wir können Vektoren ignorieren, bei denen der erste? = 1
, weil derA * cycled(B) > 0
, für alle diese Vektoren gilt. Ich hoffe du kannst folgen. Es ist wahrscheinlich nicht das beste Beispiel.Damit kann ich
n = 15
in 6 Minuten rechnen .bearbeiten 4:
Schnell umgesetzt kuroi Nekos große Idee, die besagt, dass
B
und-B
erzeugt die gleichen Ergebnisse. Beschleunigung x2. Die Implementierung ist jedoch nur ein kurzer Hack.n = 15
in 3 minuten.Code:
Den vollständigen Code finden Sie unter Github . Der folgende Code ist nur eine Darstellung der Hauptfunktionen. Ich habe Importe weggelassen, Multicore-Programmierung, Drucken der Ergebnisse, ...
Verwendung:
Du musst pypy installieren (für Python 2 !!!). Das parallele Python-Modul ist nicht für Python 3 portiert. Anschließend müssen Sie das parallele Python-Modul pp-1.6.4.zip installieren . Extrahieren Sie es
cd
in den Ordner und rufen Siepypy setup.py install
.Dann kannst du mein Programm mit aufrufen
pypy you-do-the-math.py 15
Es wird automatisch die Anzahl der CPUs ermittelt. Nach dem Beenden des Programms kann es zu Fehlermeldungen kommen. Ignorieren Sie diese einfach.
n = 16
sollte auf Ihrem Rechner möglich sein.Ausgabe:
Notizen und Ideen:
A & B
Blöcke 01 und 10 berechnen und zählen. Radfahren kann durch Verschieben des Vektors und Verwenden von Masken erfolgen. Ich habe das alles tatsächlich implementiert. Sie finden es in einigen meiner älteren Commits auf Github. Es stellte sich aber heraus, langsamer zu sein als mit Listen. Ich denke, pypy optimiert die Listenoperationen wirklich.quelle
Wollmobber - C ++ - viel zu langsam
Nun, da ein besserer Programmierer die Implementierung von C ++ übernommen hat, rufe ich für dieses Programm das Beenden auf.
Erstellen der ausführbaren Datei
Es ist eine eigenständige C ++ 11-Quelle, die ohne Warnungen kompiliert und problemlos ausgeführt werden kann:
Wenn Sie mit g ++ kompilieren, geben Sie Folgendes ein : g ++ -O3 -pthread -std = c ++ 11 Wenn Sie etwas
vergessen,
-pthread
wird ein netter und benutzerfreundlicher Core-Dump erstellt.Optimierungen
Der letzte Z-Term ist gleich dem ersten (Bpre x A in beiden Fällen), so dass die letzten beiden Ergebnisse immer gleich sind, wodurch der letzte Z-Wert nicht berechnet werden muss.
Der Gewinn ist vernachlässigbar, aber die Codierung kostet nichts, so dass Sie es genauso gut verwenden können.
Wie Jakube entdeckte, ergeben alle zyklischen Werte eines gegebenen A-Vektors die gleichen Wahrscheinlichkeiten.
Sie können diese mit einer einzelnen Instanz von A berechnen und das Ergebnis mit der Anzahl der möglichen Umdrehungen multiplizieren. Rotationsgruppen können leicht in vernachlässigbarer Zeit vorberechnet werden, was einen enormen Netto-Geschwindigkeitsgewinn darstellt.
Da die Anzahl der Permutationen eines n-Längenvektors n-1 beträgt, sinkt die Komplexität von o (6 n ) auf o (6 n / (n-1)), was im Grunde genommen für dieselbe Rechenzeit einen Schritt weiter geht.
Es scheint, dass Paare von symmetrischen Mustern auch die gleichen Wahrscheinlichkeiten erzeugen. Zum Beispiel 100101 und 101001.
Ich habe keinen mathematischen Beweis dafür, aber intuitiv, wenn alle möglichen B-Muster dargestellt werden, wird jeder symetrische A-Wert mit dem entsprechenden symetrischen B-Wert für dasselbe globale Ergebnis verknüpft.
Dies ermöglicht es, einige weitere A-Vektoren neu zu gruppieren, um die Anzahl der A-Gruppen um ca. 30% zu verringern.
FALSCH
Aus einem halb mysteriösen Grund führen alle Muster mit nur einem oder zwei gesetzten Bits zum gleichen Ergebnis. Dies stellt nicht so viele unterschiedliche Gruppen dar, aber dennoch können sie praktisch kostenlos zusammengelegt werden.Die Vektoren B und -B (B mit allen Komponenten multipliziert mit -1) ergeben die gleichen Wahrscheinlichkeiten.
(zum Beispiel [1, 0, -1, 1] und [-1, 0, 1, -1]).
Mit Ausnahme des Nullvektors (alle Komponenten sind gleich 0) bilden B und -B ein Paar verschiedener Vektoren.
Dies ermöglicht es, die Anzahl der B-Werte zu halbieren, indem nur einer von jedem Paar betrachtet und sein Beitrag mit 2 multipliziert wird, wobei der bekannte globale Beitrag von Null B zu jeder Wahrscheinlichkeit nur einmal addiert wird.
Wie es funktioniert
Die Anzahl der B-Werte ist sehr groß (3 n ), sodass für die Vorberechnung unzureichender Speicher erforderlich ist, wodurch die Berechnung verlangsamt und der verfügbare RAM-Speicher möglicherweise erschöpft wird.
Leider konnte ich keinen einfachen Weg finden, um die Hälfte der optimierten B-Werte aufzulisten. Deshalb habe ich auf die Codierung eines dedizierten Generators zurückgegriffen.
Der mächtige B-Generator hat eine Menge Spaß beim Programmieren gemacht, obwohl Sprachen, die Ertragsmechanismen unterstützen, es erlaubt hätten, ihn viel eleganter zu programmieren.
Kurz gesagt: Betrachten Sie das "Skelett" eines Bpre-Vektors als einen binären Vektor, bei dem Einsen tatsächliche -1- oder +1-Werte darstellen.
Unter all diesen +1 / -1-Potentialwerten ist der erste fest auf +1 (wodurch einer der möglichen B / -B-Vektoren ausgewählt wird), und alle verbleibenden möglichen +1 / -1-Kombinationen werden aufgelistet.
Schließlich stellt ein einfaches Kalibrierungssystem sicher, dass jeder Worker-Thread einen Wertebereich von ungefähr derselben Größe verarbeitet.
A-Werte werden stark gefiltert, um sie in gleich wahrscheinlichen Blöcken neu zu gruppieren.
Dies geschieht in einer Vorberechnungsphase, in der Brute-Force alle möglichen Werte untersucht.
Dieser Teil hat eine vernachlässigbare O (2 n ) -Ausführungszeit und muss nicht optimiert werden (der Code ist bereits unlesbar genug!).
Um das innere Produkt (das nur gegen Null getestet werden muss) zu bewerten, werden die Komponenten -1 und 1 von B in binäre Vektoren umgruppiert.
Das innere Produkt ist null, wenn (und nur wenn) es eine gleiche Anzahl von +1 und -1 unter den B-Werten gibt, die Nicht-Null-A-Werten entsprechen.
Dies kann mit einfachen Maskierungs- und Bitzähloperationen berechnet
std::bitset
werden, wodurch ein einigermaßen effizienter Bitzählcode erzeugt wird, ohne auf hässliche intrinsische Anweisungen zurückgreifen zu müssen.Die Arbeit ist zu gleichen Teilen auf die Kerne aufgeteilt, mit erzwungener CPU-Affinität (jedes kleine bisschen hilft, so heißt es).
Beispielergebnis
Vorstellungen
Multithreading sollte perfekt funktionieren, obwohl nur "echte" Kerne zur Rechengeschwindigkeit beitragen. Meine CPU hat nur 2 Kerne für 4 CPUs und der Gewinn gegenüber einer Singlethread-Version beträgt "nur" etwa 3,5.
Compiler
Ein anfängliches Problem mit Multithreading ließ mich glauben, dass die GNU-Compiler schlechter abschnitten als Microsoft.
Nach eingehenderen Tests scheint es, als ob g ++ den Tag erneut gewinnt und ungefähr 30% schnelleren Code produziert (das gleiche Verhältnis, das ich bei zwei anderen rechenintensiven Projekten festgestellt habe).
Insbesondere ist die
std::bitset
Bibliothek mit dedizierten Bitzählanweisungen von g ++ 4.8 implementiert, während MSVC 2013 nur Schleifen herkömmlicher Bitverschiebungen verwendet.Wie zu erwarten war, macht das Kompilieren in 32 oder 64 Bit keinen Unterschied.
Weitere Verfeinerungen
Ich bemerkte ein paar A-Gruppen, die nach allen Reduktionsoperationen die gleichen Wahrscheinlichkeiten aufwiesen, konnte jedoch kein Muster identifizieren, das es mir ermöglichte, sie neu zu gruppieren.
Hier sind die Paare, die ich für n = 11 bemerkt habe:
quelle
terminate called after throwing an instance of 'std::system_error' what(): Unknown error -1 Aborted (core dumped)