Die Herausforderung besteht darin, den schnellstmöglichen Code für die Berechnung des Hafnian einer Matrix zu schreiben .
Die Hafnian einer symmetrischen 2n
-by- 2n
Matrix A
ist definiert als:
Hier repräsentiert S 2n die Menge aller Permutationen der ganzen Zahlen von 1
bis 2n
, das heißt [1, 2n]
.
Der Wikipedia-Link gibt auch eine anders aussehende Formel an, die von Interesse sein kann (und es gibt noch schnellere Methoden, wenn Sie weiter im Web suchen). Die gleiche Wiki-Seite behandelt Adjazenzmatrizen, aber Ihr Code sollte auch für andere Matrizen funktionieren. Sie können davon ausgehen, dass alle Werte Ganzzahlen sind, aber nicht, dass sie alle positiv sind.
Es gibt auch einen schnelleren Algorithmus, der jedoch schwer zu verstehen ist. und Christian Sievers war der erste, der es umsetzte (in Haskell).
In dieser Frage sind die Matrizen alle quadratisch und symmetrisch mit einer gleichmäßigen Dimension.
Referenzimplementierung (beachten Sie, dass dies die langsamstmögliche Methode verwendet).
Hier ist ein Beispiel für Python-Code von Mr. Xcoder.
from itertools import permutations
from math import factorial
def hafnian(matrix):
my_sum = 0
n = len(matrix) // 2
for sigma in permutations(range(n*2)):
prod = 1
for j in range(n):
prod *= matrix[sigma[2*j]][sigma[2*j+1]]
my_sum += prod
return my_sum / (factorial(n) * 2 ** n)
print(hafnian([[-1, 1, 1, -1, 0, 0, 1, -1], [1, 0, 1, 0, -1, 0, -1, -1], [1, 1, -1, 1, -1, -1, 0, -1], [-1, 0, 1, -1, -1, 1, -1, 0], [0, -1, -1, -1, -1, 0, 0, -1], [0, 0, -1, 1, 0, 0, 1, 1], [1, -1, 0, -1, 0, 1, 1, 0], [-1, -1, -1, 0, -1, 1, 0, 1]]))
4
M = [[1, 1, 0, 0, 0, 0, 0, 1, 0, 0], [1, 1, -1, 0, -1, 1, 1, 1, 0, -1], [0, -1, -1, -1, 0, -1, -1, 0, -1, 1], [0, 0, -1, 1, -1, 1, -1, 0, 1, -1], [0, -1, 0, -1, -1, -1, -1, 1, -1, 1], [0, 1, -1, 1, -1, 1, -1, -1, 1, -1], [0, 1, -1, -1, -1, -1, 1, 0, 0, 0], [1, 1, 0, 0, 1, -1, 0, 1, 1, -1], [0, 0, -1, 1, -1, 1, 0, 1, 1, 1], [0, -1, 1, -1, 1, -1, 0, -1, 1, 1]]
print(hafnian(M))
-13
M = [[-1, 0, -1, -1, 0, -1, 0, 1, -1, 0, 0, 0], [0, 0, 0, 0, 0, -1, 0, 1, -1, -1, -1, -1], [-1, 0, 0, 1, 0, 0, 0, 1, -1, 1, -1, 0], [-1, 0, 1, -1, 1, -1, -1, -1, 0, -1, -1, -1], [0, 0, 0, 1, 0, 0, 0, 0, 0, 1, -1, 0], [-1, -1, 0, -1, 0, 0, 1, 1, 1, 1, 1, 0], [0, 0, 0, -1, 0, 1, 1, -1, -1, 0, 1, 0], [1, 1, 1, -1, 0, 1, -1, 1, -1, -1, -1, -1], [-1, -1, -1, 0, 0, 1, -1, -1, -1, 1, -1, 0], [0, -1, 1, -1, 1, 1, 0, -1, 1, -1, 1, 1], [0, -1, -1, -1, -1, 1, 1, -1, -1, 1, 0, -1], [0, -1, 0, -1, 0, 0, 0, -1, 0, 1, -1, 1]]
print(hafnian(M))
13
M = [[-1, 1, 0, 1, 0, -1, 0, 0, -1, 1, -1, 1, 0, -1], [1, -1, 1, -1, 1, 1, -1, 0, -1, 1, 1, 0, 0, -1], [0, 1, 1, 1, -1, 1, -1, -1, 0, 0, -1, 0, -1, -1], [1, -1, 1, -1, 1, 0, 1, 1, -1, -1, 0, 0, 1, 1], [0, 1, -1, 1, 0, 1, 0, 1, -1, -1, 1, 1, 0, -1], [-1, 1, 1, 0, 1, 1, -1, 0, 1, -1, -1, -1, 1, -1], [0, -1, -1, 1, 0, -1, -1, -1, 0, 1, -1, 0, 1, -1], [0, 0, -1, 1, 1, 0, -1, 0, 0, -1, 0, 0, 0, 1], [-1, -1, 0, -1, -1, 1, 0, 0, 1, 1, 0, 1, -1, 0], [1, 1, 0, -1, -1, -1, 1, -1, 1, 1, 1, 0, 1, 0], [-1, 1, -1, 0, 1, -1, -1, 0, 0, 1, -1, 0, -1, 0], [1, 0, 0, 0, 1, -1, 0, 0, 1, 0, 0, 1, 1, 1], [0, 0, -1, 1, 0, 1, 1, 0, -1, 1, -1, 1, 1, -1], [-1, -1, -1, 1, -1, -1, -1, 1, 0, 0, 0, 1, -1, -1]]
print(hafnian(M))
83
Die Aufgabe
Sie sollten Code schreiben, der, gegeben 2n
durch 2n
Matrix, sein Hafnian ausgibt.
Da ich Ihren Code testen muss, wäre es hilfreich, wenn Sie mir eine einfache Möglichkeit geben könnten, eine Matrix als Eingabe für Ihren Code zu verwenden, z. B. durch Einlesen des Standards. Ich werde Ihren Code in zufällig ausgewählten Matrizen mit Elementen testen ausgewählt aus {-1, 0, 1}. Der Zweck solcher Tests ist es, die Wahrscheinlichkeit zu verringern, dass der Hafnianer einen sehr großen Wert hat.
Idealerweise kann Ihr Code Matrizen genau so einlesen, wie ich sie in den Beispielen in dieser Frage angegeben habe. So würde die Eingabe [[1,-1],[-1,-1]]
beispielsweise aussehen . Wenn Sie ein anderes Eingabeformat verwenden möchten, fragen Sie bitte und ich werde mein Bestes tun, um dies zu berücksichtigen.
Partituren und Krawatten
Ich werde Ihren Code auf zufälligen Matrizen von zunehmender Größe testen und stoppen, wenn Ihr Code zum ersten Mal länger als 1 Minute auf meinem Computer dauert. Die Bewertungsmatrizen werden für alle Einreichungen konsistent sein, um die Fairness zu gewährleisten.
Wenn zwei Personen die gleiche Punktzahl erzielen, ist der Gewinner derjenige, der für diesen Wert von am schnellsten ist n
. Wenn diese innerhalb einer Sekunde voneinander entfernt sind, ist dies die zuerst veröffentlichte.
Sprachen und Bibliotheken
Sie können jede verfügbare Sprache und Bibliothek verwenden, die Sie möchten, aber keine bereits vorhandene Funktion, um das Hafnian zu berechnen. Wo immer möglich, wäre es gut, wenn Sie Ihren Code ausführen könnten. Fügen Sie daher bitte eine vollständige Erklärung dazu bei, wie Sie Ihren Code unter Linux ausführen / kompilieren, wenn dies überhaupt möglich ist. "
Mein Computer Die Timings werden auf meinem 64-Bit-Computer ausgeführt. Dies ist eine Ubuntu-Standardinstallation mit 8 GB RAM, AMD FX-8350 Eight-Core-Prozessor und Radeon HD 4250. Dies bedeutet auch, dass ich in der Lage sein muss, Ihren Code auszuführen.
Fordern Sie Antworten in mehr Sprachen
Es wäre toll, Antworten in Ihrer bevorzugten superschnellen Programmiersprache zu erhalten. So starten Sie die Dinge aus, wie etwa Fortran , nim und Rost ?
Bestenliste
- 52 Meilen mit C ++ . 30 Sekunden.
- 50 durch NGN Verwendung C . 50 Sekunden.
- 46 von Christian Sievers mit Haskell . 40 Sekunden.
- 40 Meilen mit Python 2 + Pypy . 41 Sekunden.
- 34 von ngn mit Python 3 + pypy . 29 Sekunden.
- 28 von Dennis mit Python 3 . 35 Sekunden. (Pypy ist langsamer)
Antworten:
Haskell
Dies implementiert eine Variation von Algorithmus 2 von Andreas Björklund: Das Zählen perfekter Übereinstimmungen so schnell wie Ryser .
Kompilieren Sie
ghc
mit den-O3 -threaded
Optionen+RTS -N
für die Kompilierungszeit und verwenden Sie die Laufzeitoptionen für die Parallelisierung. Übernimmt die Eingabe von stdin.quelle
parallel
undvector
muss installiert werden?Python 3
Probieren Sie es online!
quelle
C ++ (gcc)
Probieren Sie es online! (13s für n = 24)
Basiert auf der schnelleren Python- Implementierung in meinem anderen Beitrag. Bearbeiten Sie die zweite Zeile zu
#define S 3
auf Ihrem 8-Core-Rechner und kompilieren Sie mitg++ -pthread -march=native -O2 -ftree-vectorize
.Das halbiert die Arbeit, also sollte der Wert von
S
seinlog2(#threads)
. Die Typen können leicht zwischen geändert werdenint
,long
,float
unddouble
durch den Wert des Modifizieren#define TYPE
.quelle
tr -d \ < matrix52.txt > matrix52s.txt
Python 3
Dies berechnet haf (A) als memoisierte Summe (A [i] [j] * haf (A ohne Zeilen und Spalten i und j)).
quelle
C
Ein weiteres Werkzeug aus Andreas Björklunds Arbeit , das viel einfacher zu verstehen ist, wenn man sich auch Christian Sievers 'Haskell-Code ansieht . In den ersten Stufen der Rekursion werden Round-Robin-Threads auf die verfügbaren CPUs verteilt. Die letzte Stufe der Rekursion, die die Hälfte der Aufrufe ausmacht, wird von Hand optimiert.
Kompilieren mit:
gcc -O3 -pthread -march=native
; danke @Dennis für ein 2x Speed-upn = 24 in 24s bei TIO
Algorithmus:
Die symmetrische Matrix wird in Form eines Dreiecks unten links gespeichert. Dreieck-Indizes
i,j
entsprechen dem linearen Index,T(max(i,j))+min(i,j)
für denT
ein Makro stehti*(i-1)/2
. Matrixelemente sind Polynome vom Gradn
. Ein Polynom wird dargestellt als eine Anordnung von Koeffizienten aus dem konstanten Term (geordnetep[0]
) bis x n ‚s Koeffizienten (p[n]
). Die anfänglichen -1,0,1-Matrixwerte werden zuerst in konstante Polynome umgewandelt.Wir führen einen rekursiven Schritt mit zwei Argumenten durch: der Halbmatrix (dh dem Dreieck)
a
von Polynomen und einem separaten Polynomp
(im Artikel als Beta bezeichnet). Wir reduzieren das Größenproblemm
(anfangsm=2*n
) rekursiv auf zwei Größenproblemem-2
und geben die Differenz ihrer Hafnier zurück. Eine davon ist, dass Sie dasselbea
ohne die letzten beiden Zeilen und dasselbe verwendenp
. Eine andere Möglichkeit ist die Verwendung des Dreiecksb[i][j] = a[i][j] + shmul(a[m-1][i],a[m-2][j]) + shmul(a[m-1][j],a[m-2][i])
(bei dershmul
die Verschiebungs-Multiplikations-Operation für Polynome ausgeführt wird - es ist wie üblich ein Polynomprodukt, zusätzlich multipliziert mit der Variablen "x"; Potenzen über x ^ n werden ignoriert) und des separaten Polynomsq = p + shmul(p,a[m-1][m-2])
. Wenn die Rekursion eine Größe von 0 erreichta
, geben wir den Hauptkoeffizienten von p: zurückp[n]
.Die Verschiebe- und Multiplikationsoperation ist in Funktion implementiert
f(x,y,z)
. Es ändert sichz
an Ort und Stelle. Im Grunde genommen schonz += shmul(x,y)
. Dies scheint der leistungskritischste Teil zu sein.Nachdem die Rekursion beendet ist, müssen wir das Vorzeichen des Ergebnisses durch Multiplikation mit (-1) n korrigieren .
quelle
-march=native
wird hier einen großen Unterschied machen. Zumindest bei TIO halbiert es fast die Wandzeit.Python
Dies ist so ziemlich eine direkte Referenzimplementierung von Algorithmus 2 aus dem erwähnten Artikel . Die einzigen Änderungen bestanden darin, nur den aktuellen Wert von B beizubehalten, die Werte von β zu löschen, indem nur g aktualisiert wurde, wenn i ∈ X ist , und die Multiplikation mit abgeschnittenen Polynomen durchzuführen, indem nur die Werte bis zum Grad n berechnet wurden .
Probieren Sie es online!
Hier ist eine schnellere Version mit einigen der einfachen Optimierungen.
Probieren Sie es online!
Für noch mehr Spaß, hier ist eine Referenz Implementierung in J.
quelle
pmul
, verwendenfor j in range(n-i):
und vermeiden Sie dieif
j != k
mitj < k
. Es kopiert eine Submatrix in den else-Fall, was vermieden werden kann, wenn die letzten beiden statt der ersten beiden Zeilen und Spalten behandelt und gelöscht werden. Und wenn es mitx={1,2,4}
und später mitx={1,2,4,6}
rechnet, wiederholt es seine Berechnungen bis zui=5
. Ich habe die Struktur der beiden äußeren Schleifen ersetzt,i
indem ich zuerst eine Schleife angelegt habe und dann rekursivi in X
und angenommen habei not in X
. - Übrigens, es könnte interessant sein, das Wachstum der benötigten Zeit im Vergleich zu den anderen langsameren Programmen zu betrachten.Oktave
Dies ist im Grunde eine Kopie von Dennis 'Eintrag , aber optimiert für Octave. Die Hauptoptimierung erfolgt unter Verwendung der vollständigen Eingabematrix (und ihrer Transponierung) und Rekursion, wobei nur Matrixindizes verwendet werden, anstatt reduzierte Matrizen zu erstellen.
Der Hauptvorteil ist das reduzierte Kopieren von Matrizen. Während Octave keinen Unterschied zwischen Zeigern / Referenzen und Werten hat und funktional nur einen vorübergehenden Wert hat, ist es eine andere Geschichte hinter den Kulissen. Dort wird Copy-on-Write (Lazy Copy) verwendet. Das heißt, für den Code
a=1;b=a;b=b+1
wird die Variableb
erst bei der letzten Anweisung an eine neue Position kopiert, wenn sie geändert wird. Damatin
undmatransp
werden nie geändert, werden sie nie kopiert. Der Nachteil ist, dass die Funktion mehr Zeit für die Berechnung der korrekten Indizes benötigt. Möglicherweise muss ich verschiedene Variationen zwischen numerischen und logischen Indizes ausprobieren, um dies zu optimieren.Wichtiger Hinweis: Eingabematrix sollte sein
int32
! Speichern Sie die Funktion in einer Datei mit dem Namenhaf.m
Beispiel-Testskript:
Ich habe das mit TIO und MATLAB ausprobiert (Octave habe ich eigentlich nie installiert). Ich kann mir vorstellen, dass es so einfach ist, es zum Laufen zu bringen
sudo apt-get install octave
. Der Befehloctave
lädt die Octave-GUI. Wenn es komplizierter ist, werde ich diese Antwort löschen, bis ich eine ausführlichere Installationsanleitung zur Verfügung gestellt habe.quelle
Kürzlich haben Andreas Bjorklund, Brajesh Gupt und ich einen neuen Algorithmus für Hafnianer komplexer Matrizen veröffentlicht: https://arxiv.org/pdf/1805.12498.pdf . Für eine n \ times n-Matrix skaliert sie wie n ^ 3 2 ^ {n / 2}.
Wenn ich den ursprünglichen Algorithmus von Andreas von https://arxiv.org/pdf/1107.4466.pdf richtig verstehe, wird er wie folgt skaliert: n ^ 4 2 ^ {n / 2} oder n ^ 3 log (n) 2 ^ {n / 2} Wenn Sie Fouriertransformationen für Polynommultiplikationen verwendet haben.
Unser Algorithmus ist speziell auf komplexe Matrizen zugeschnitten, sodass er nicht so schnell ist wie die hier für {-1,0,1} Matrizen entwickelten. Ich frage mich jedoch, ob man einige der Tricks anwenden kann, mit denen Sie unsere Implementierung verbessert haben. Auch wenn die Leute interessiert sind, würde ich gerne sehen, wie sich ihre Implementierung verhält, wenn komplexe Zahlen anstelle von ganzen Zahlen verwendet werden. Schließlich sind Kommentare, Kritik, Verbesserungen, Bugs und Verbesserungen in unserem Repository https://github.com/XanaduAI/hafnian/ willkommen.
Prost!
quelle