Le défi est d'écrire le code le plus rapide possible pour calculer le Hafnien d'une matrice .
Le Hafnian d'une matrice symétrique 2n
-par- est défini comme:2n
A
Ici S 2n représente l'ensemble de toutes les permutations des entiers de 1
à 2n
, c'est-à-dire [1, 2n]
.
Le lien wikipedia donne également une formule différente qui peut être intéressante (et des méthodes encore plus rapides existent si vous regardez plus loin sur le web). La même page wiki parle des matrices de contiguïté mais votre code devrait également fonctionner pour d'autres matrices. Vous pouvez supposer que les valeurs seront toutes des entiers mais pas qu'elles soient toutes positives.
Il existe également un algorithme plus rapide mais il semble difficile à comprendre. et Christian Sievers a été le premier à le mettre en œuvre (à Haskell).
Dans cette question, les matrices sont toutes carrées et symétriques avec une dimension paire.
Implémentation de référence (notez que cela utilise la méthode la plus lente possible).
Voici un exemple de code python de M. 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
La tâche
Vous devez écrire du code qui, donné 2n
par une 2n
matrice, génère son Hafnian.
Comme je devrai tester votre code, il serait utile que vous me donniez un moyen simple de donner une matrice en entrée à votre code, par exemple en lisant à partir de standard in. Je testerai votre code dans des matrices choisies au hasard avec des éléments sélectionné parmi {-1, 0, 1}. Le but de tests comme celui-ci est de réduire les chances que le Hafnian soit d'une très grande valeur.
Idéalement, votre code sera capable de lire dans les matrices exactement comme je les ai dans les exemples de cette question directement à partir de la norme en. C'est-à-dire que l'entrée ressemblerait [[1,-1],[-1,-1]]
par exemple. Si vous souhaitez utiliser un autre format d'entrée, veuillez demander et je ferai de mon mieux pour s'adapter.
Scores et égalités
Je vais tester votre code sur des matrices aléatoires de taille croissante et arrêter la première fois que votre code prend plus d'une minute sur mon ordinateur. Les matrices de notation seront cohérentes pour toutes les soumissions afin d'assurer l'équité.
Si deux personnes obtiennent le même score, le gagnant est celui qui est le plus rapide pour cette valeur de n
. Si ceux-ci sont à moins d'une seconde les uns des autres, c'est celui affiché en premier.
Langues et bibliothèques
Vous pouvez utiliser n'importe quelle langue et bibliothèque disponibles, mais aucune fonction préexistante pour calculer le hafnien. Dans la mesure du possible, il serait bon de pouvoir exécuter votre code, veuillez donc inclure une explication complète sur la façon d'exécuter / compiler votre code sous Linux si possible. »
Ma machine Les temporisations seront exécutées sur ma machine 64 bits. Il s'agit d'une installation Ubuntu standard avec 8 Go de RAM, processeur AMD FX-8350 à huit cœurs et Radeon HD 4250. Cela signifie également que je dois pouvoir exécuter votre code.
Appel à réponses dans plus de langues
Ce serait formidable d'obtenir des réponses dans votre langage de programmation super rapide préféré. Pour commencer, que diriez-vous du fortran , du nim et de la rouille ?
Classement
- 52 par miles en utilisant C ++ . 30 secondes.
- 50 en utilisant ngn C . 50 secondes.
- 46 par Christian Sievers utilisant Haskell . 40 secondes.
- 40 miles en utilisant Python 2 + pypy . 41 secondes.
- 34 par ngn en utilisant Python 3 + pypy . 29 secondes.
- 28 par Dennis en utilisant Python 3 . 35 secondes. (Pypy est plus lent)
Réponses:
Haskell
Cela implémente une variante de l'algorithme 2 d' Andreas Björklund: compter les correspondances parfaites aussi vite que Ryser .
Compilez à l'aide des
ghc
options de compilation-O3 -threaded
et utilisez les options+RTS -N
d' exécution pour la parallélisation. Prend l'entrée de stdin.la source
parallel
etvector
doit être installé?Python 3
Essayez-le en ligne!
la source
C ++ (gcc)
Essayez-le en ligne! (13s pour n = 24)
Basé sur la mise en œuvre plus rapide de Python dans mon autre article. Modifiez la deuxième ligne
#define S 3
sur votre machine à 8 cœurs et compilez avecg++ -pthread -march=native -O2 -ftree-vectorize
.Le divise le travail en deux, donc la valeur de
S
devrait êtrelog2(#threads)
. Les types peuvent facilement être changés entreint
,long
,float
etdouble
en modifiant la valeur#define TYPE
.la source
tr -d \ < matrix52.txt > matrix52s.txt
Python 3
Ceci calcule haf (A) comme une somme mémorisée (A [i] [j] * haf (A sans lignes et cols i et j)).
la source
C
Un autre élément du document d' Andreas Björklund , qui est beaucoup plus facile à comprendre si vous regardez également le code Haskell de Christian Sievers . Pour les premiers niveaux de la récursivité, il distribue les threads à tour de rôle sur les processeurs disponibles. Le dernier niveau de la récursivité, qui représente la moitié des invocations, est optimisé à la main.
Compiler avec:
gcc -O3 -pthread -march=native
; merci @Dennis pour une accélération 2xn = 24 en 24s sur TIO
Algorithme:
La matrice, qui est symétrique, est stockée sous forme triangulaire en bas à gauche. Les indices triangulaires
i,j
correspondent à l'indice linéaireT(max(i,j))+min(i,j)
oùT
est une macro pouri*(i-1)/2
. Les éléments matriciels sont des polynômes de degrén
. Un polynôme est représenté comme un tableau de coefficients ordonnés du terme constant (p[0]
) au coefficient de x n (p[n]
). Les valeurs initiales de la matrice -1,0,1 sont d'abord converties en polynômes const.Nous effectuons une étape récursive avec deux arguments: la demi-matrice (c'est-à-dire le triangle)
a
des polynômes et un polynôme séparép
(appelé bêta dans l'article). Nous réduisons lem
problème de taille (initialementm=2*n
) récursivement à deux problèmes de taillem-2
et retournons la différence de leurs hafniens. L'un d'eux est d'utiliser le mêmea
sans ses deux dernières lignes, et le mêmep
. Une autre consiste à utiliser le triangleb[i][j] = a[i][j] + shmul(a[m-1][i],a[m-2][j]) + shmul(a[m-1][j],a[m-2][i])
(où seshmul
trouve l'opération de décalage-multiplication sur les polynômes - c'est comme le produit polynomial comme d'habitude, en plus multiplié par la variable "x"; les puissances supérieures à x ^ n sont ignorées), et le polynôme séparéq = p + shmul(p,a[m-1][m-2])
. Lorsque la récursivité frappe une taille 0a
, nous revenons le grand coefficient de p:p[n]
.L'opération shift-and-multiply est implémentée en fonction
f(x,y,z)
. Il modifiez
sur place. En gros, c'est le casz += shmul(x,y)
. Cela semble être la partie la plus critique en termes de performances.Une fois la récursivité terminée, nous devons fixer le signe du résultat en multipliant par (-1) n .
la source
-march=native
fera une grande différence ici. Au moins sur TIO, il réduit presque de moitié le temps de mur.Python
Il s'agit d'une implémentation de référence simple de l'algorithme 2 du document mentionné . Les seules modifications consistaient à ne conserver que la valeur actuelle de B , à supprimer les valeurs de β en ne mettant à jour g que lorsque i ∈ X , et à multiplier les polynômes tronqués en calculant uniquement les valeurs jusqu'au degré n .
Essayez-le en ligne!
Voici une version plus rapide avec certaines des optimisations faciles.
Essayez-le en ligne!
Pour plus de plaisir, voici une implémentation de référence en J.
la source
pmul
, utilisezfor j in range(n-i):
et évitez leif
j != k
parj < k
. Il copie une sous-matrice dans le cas contraire, ce qui peut être évité lorsque nous traitons et supprimons les deux derniers au lieu des deux premières lignes et colonnes. Et quand il calcule avecx={1,2,4}
et plus tard avecx={1,2,4,6}
alors il répète ses calculs jusqu'ài=5
. J'ai remplacé la structure des deux boucles externes avec un premier bouclagei
puis en supposant récursivementi in X
eti not in X
. - BTW, Il pourrait être intéressant de regarder la croissance du temps nécessaire par rapport aux autres programmes plus lents.Octave
Il s'agit essentiellement d'une copie de l'entrée de Dennis , mais optimisée pour Octave. L'optimisation principale se fait en utilisant la matrice d'entrée complète (et sa transposition) et la récursivité en utilisant uniquement des indices de matrice, plutôt que de créer des matrices réduites.
Le principal avantage est une copie réduite des matrices. Bien qu'Octave n'ait pas de différence entre les pointeurs / références et les valeurs et ne fonctionne que par valeur, c'est une histoire différente dans les coulisses. Là, la copie sur écriture (copie paresseuse) est utilisée. Cela signifie que, pour le code
a=1;b=a;b=b+1
, la variableb
n'est copiée vers un nouvel emplacement qu'à la dernière instruction, lorsqu'elle est modifiée. Depuismatin
etmatransp
ne sont jamais modifiés, ils ne seront jamais copiés. L'inconvénient est que la fonction passe plus de temps à calculer les bons indices. Je devrai peut-être essayer différentes variations entre les indices numériques et logiques pour optimiser cela.Remarque importante: la matrice d'entrée devrait l'être
int32
! Enregistrez la fonction dans un fichier appeléhaf.m
Exemple de script de test:
J'ai essayé cela en utilisant TIO et MATLAB (je n'ai en fait jamais installé Octave). J'imagine que le faire fonctionner est aussi simple que
sudo apt-get install octave
. La commandeoctave
chargera l'interface graphique d'Octave. Si c'est plus compliqué que cela, je supprimerai cette réponse jusqu'à ce que j'aie fourni des instructions d'installation plus détaillées.la source
Récemment, Andreas Bjorklund, Brajesh Gupt et moi-même avons publié un nouvel algorithme pour les Hafniens de matrices complexes: https://arxiv.org/pdf/1805.12498.pdf . Pour une matrice n \ fois n, elle évolue comme n ^ 3 2 ^ {n / 2}.
Si je comprends bien l'algorithme original d'Andreas de https://arxiv.org/pdf/1107.4466.pdf, il évolue comme n ^ 4 2 ^ {n / 2} ou n ^ 3 log (n) 2 ^ {n / 2} si vous avez utilisé des transformées de Fourier pour effectuer des multiplications polynomiales.
Notre algorithme est spécifiquement adapté aux matrices complexes, il ne sera donc pas aussi rapide que ceux développés ici pour les matrices {-1,0,1}. Je me demande cependant si on peut utiliser certaines des astuces que vous avez utilisées pour améliorer notre implémentation? De plus, si les gens sont intéressés, j'aimerais voir comment leur implémentation fonctionne quand on leur donne des nombres complexes au lieu d'entiers. Enfin, tous les commentaires, critiques, améliorations, bugs, améliorations sont les bienvenus dans notre référentiel https://github.com/XanaduAI/hafnian/
À votre santé!
la source