Wyzwanie polega na napisaniu najszybszego możliwego kodu do obliczenia Hafniana matrycy .
Hafnian symetrycznego 2n
-by- 2n
matrycę A
określa się jako:
Tutaj S 2n reprezentuje zestaw wszystkich permutacji liczb całkowitych od 1
do 2n
, to znaczy [1, 2n]
.
Link do wikipedii daje również inną formułę, która może być interesująca (a jeszcze szybsze metody istnieją, jeśli spojrzysz dalej w Internecie). Ta sama strona wiki mówi o macierzach przylegania, ale twój kod powinien również działać dla innych macierzy. Możesz założyć, że wszystkie wartości będą liczbami całkowitymi, ale nie że wszystkie są dodatnie.
Istnieje również szybszy algorytm, ale wydaje się trudny do zrozumienia. a Christian Sievers jako pierwszy wdrożył go (w Haskell).
W tym pytaniu wszystkie macierze są kwadratowe i symetryczne o równych wymiarach.
Implementacja referencyjna (należy pamiętać, że jest to najwolniejsza możliwa metoda).
Oto przykładowy kod python od pana Xcodera.
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
Zadanie
Powinieneś napisać kod, który podany 2n
przez 2n
macierz, wyświetla swój Hafnian.
Ponieważ będę musiał przetestować kod, pomocne byłoby podanie prostego sposobu na przekazanie macierzy jako danych wejściowych do kodu, na przykład poprzez czytanie ze standardowego wejścia. Przetestuję kod w losowo wybranych matrycach z elementami wybrane z {-1, 0, 1}. Celem takich testów jest zmniejszenie szansy, że Hafnian będzie miał bardzo dużą wartość.
Idealnie twój kod będzie mógł czytać w matrycach dokładnie tak, jak ja je w przykładach w tym pytaniu prosto ze standardowego w. To byłoby wejście, które wyglądałoby [[1,-1],[-1,-1]]
na przykład. Jeśli chcesz użyć innego formatu wejściowego, zapytaj, a ja dołożę wszelkich starań, aby to uwzględnić.
Wyniki i krawaty
Przetestuję twój kod na losowych matrycach o rosnącym rozmiarze i zatrzymam się, gdy twój kod po raz pierwszy zajmie więcej niż 1 minutę na moim komputerze. Macierze oceny będą spójne dla wszystkich wniosków, aby zapewnić uczciwość.
Jeśli dwie osoby otrzymają ten sam wynik, zwycięzcą jest ta, która jest najszybsza dla tej wartości n
. Jeśli są one w odległości 1 sekundy od siebie, to jest to pierwszy.
Języki i biblioteki
Możesz użyć dowolnego dostępnego języka i bibliotek, które ci się podobają, ale nie ma wcześniejszej funkcji do obliczenia Hafniana. Tam, gdzie to możliwe, dobrze byłoby móc uruchomić kod, więc proszę podać pełne wyjaśnienie, w jaki sposób uruchomić / skompilować kod w systemie Linux, jeśli to w ogóle możliwe.
Moja maszyna Czasy zostaną uruchomione na moim komputerze 64-bitowym. Jest to standardowa instalacja ubuntu z 8 GB pamięci RAM, ośmiordzeniowym procesorem AMD FX-8350 i Radeon HD 4250. Oznacza to również, że muszę mieć możliwość uruchomienia kodu.
Zadzwoń po więcej języków
Byłoby wspaniale uzyskać odpowiedzi w swoim ulubionym, bardzo szybkim języku programowania. Na początek, co powiesz na fortran , nim i rdzę ?
Tabela liderów
- 52 przez mile przy użyciu C ++ . 30 sekund.
- 50 przy użyciu NGN C . 50 sekund.
- 46 Christian Sievers korzystający z Haskell . 40 sekund.
- 40 mil za pomocą Python 2 + pypy . 41 sekund.
- 34 przez ngn przy użyciu Python 3 + pypy . 29 sekund.
- 28 autorstwa Dennisa przy użyciu Pythona 3 . 35 sekund. (Pypy jest wolniejszy)
Odpowiedzi:
Haskell
To implementuje odmianę algorytmu 2 Andreasa Björklunda: liczenie idealnych dopasowań tak szybko jak Ryser .
Kompiluj za
ghc
pomocą opcji czasu kompilacji-O3 -threaded
i używaj opcji czasu wykonywania+RTS -N
do równoległości. Pobiera dane wejściowe ze standardowego wejścia.źródło
parallel
ivector
musi być zainstalowane?Python 3
Wypróbuj online!
źródło
C ++ (gcc)
Wypróbuj online! (13s dla n = 24)
Na podstawie szybszej implementacji Pythona w moim innym poście. Edytuj drugi wiersz
#define S 3
na 8-rdzeniowej maszynie i skompiluj zg++ -pthread -march=native -O2 -ftree-vectorize
.Dzieli pracę na pół, więc wartość
S
powinna wynosićlog2(#threads)
. Typy można łatwo zmieniać pomiędzyint
,long
,float
, idouble
modyfikując wartość#define TYPE
.źródło
tr -d \ < matrix52.txt > matrix52s.txt
Python 3
Oblicza haf (A) jako zapamiętaną sumę (A [i] [j] * haf (A bez wierszy i kolumn i oraz j)).
źródło
do
Kolejny wgląd w pracę Andreasa Björklunda , który jest znacznie łatwiejszy do zrozumienia, jeśli spojrzysz również na kod Haskella Christiana Sieversa . Przez pierwsze kilka poziomów rekurencji rozdziela on wątki typu round-robin na dostępne procesory. Ostatni poziom rekurencji, który stanowi połowę inwokacji, jest optymalizowany ręcznie.
Skompilować z:
gcc -O3 -pthread -march=native
; dzięki @Dennis za 2x przyspieszenien = 24 na 24 sekundy w TIO
Algorytm:
Matryca, która jest symetryczna, jest przechowywana w lewym dolnym trójkątnym kształcie. Wskaźniki trójkąta
i,j
odpowiadają indeksowi liniowemu, dlaT(max(i,j))+min(i,j)
któregoT
jest makremi*(i-1)/2
. Elementy macierzy są wielomianami stopnian
. Wielomian jest reprezentowany jako tablica współczynników uporządkowanych od stałego składnika (p[0]
) do współczynnika x n (p[n]
). Początkowe wartości macierzy -1,0,1 są najpierw konwertowane na wielomiany const.Wykonujemy krok rekurencyjny z dwoma argumentami: pół macierzą (tj. Trójkątem)
a
wielomianów i oddzielnym wielomianemp
(w artykule określanym jako beta). Zmniejszamym
problem wielkości (początkowom=2*n
) rekurencyjnie do dwóch problemów wielkościm-2
i zwracamy różnicę u ich hafnianów. Jednym z nich jest użycie tego samegoa
bez dwóch ostatnich rzędów i to samop
. Innym jest użycie trójkątab[i][j] = a[i][j] + shmul(a[m-1][i],a[m-2][j]) + shmul(a[m-1][j],a[m-2][i])
(gdzieshmul
jest operacja mnożenia z przesunięciem na wielomianach - to jak zwykle iloczyn wielomianowy, dodatkowo pomnożony przez zmienną „x”; moce wyższe niż x ^ n są ignorowane) i oddzielny wielomianq = p + shmul(p,a[m-1][m-2])
. Kiedy rekurencja uderza wielkość-0a
, wracamy główną współczynnik p:p[n]
.Operacja przełączania i mnożenia jest realizowana w funkcji
f(x,y,z)
. Zmieniaz
na miejscu. Mówiąc luźno, to prawdaz += shmul(x,y)
. To wydaje się być najbardziej krytyczną częścią wydajności.Po zakończeniu rekurencji musimy naprawić znak wyniku, mnożąc przez (-1) n .
źródło
-march=native
zrobi dużą różnicę tutaj. Przynajmniej w TIO prawie skraca czas ściany o połowę.Pyton
Jest to w zasadzie prosta referencyjna implementacja algorytmu 2 ze wspomnianego artykułu . Jedynymi zmianami było utrzymanie bieżącej wartości B , opuszczenie wartości p tylko przez aktualizujący g podczas i ∈ X i ściętego wielomian mnożenie tylko przez obliczenie wartości do stopnia n .
Wypróbuj online!
Oto szybsza wersja z kilkoma łatwymi optymalizacjami.
Wypróbuj online!
Dla dodatkowej zabawy, oto implementacja referencyjna w J.
źródło
pmul
, używajfor j in range(n-i):
i unikajif
j != k
zj < k
. Kopiuje submatriks w innym przypadku, którego można uniknąć, gdy zajmiemy się i usuniemy dwa ostatnie zamiast dwóch pierwszych wierszy i kolumn. A kiedy oblicza zix={1,2,4}
późniejx={1,2,4,6}
, powtarza swoje obliczenia doi=5
. Strukturę dwóch zewnętrznych pętli zastąpiłem najpierw zapętleniem,i
a następnie rekurencyjnie zakładająci in X
ii not in X
. - BTW, Ciekawie byłoby spojrzeć na wzrost potrzebnego czasu w porównaniu z innymi wolniejszymi programami.Oktawa
Jest to w zasadzie kopia wpisu Dennisa , ale zoptymalizowana dla Octave. Głównej optymalizacji dokonuje się przy użyciu pełnej macierzy wejściowej (i jej transpozycji) i rekurencji przy użyciu tylko wskaźników macierzowych, zamiast tworzenia zredukowanych macierzy.
Główną zaletą jest ograniczenie kopiowania matryc. Podczas gdy Octave nie ma różnicy między wskaźnikami / referencjami a wartościami, a funkcjonalnie tylko przekazuje wartość, to inna historia za kulisami. Tam stosuje się kopiowanie przy zapisie (leniwa kopia). Oznacza to, że dla kodu
a=1;b=a;b=b+1
zmiennab
jest kopiowana tylko do nowej lokalizacji w ostatniej instrukcji, gdy jest zmieniana. Odmatin
imatransp
nigdy nie są zmieniane, nigdy nie zostaną skopiowane. Wadą jest to, że funkcja spędza więcej czasu na obliczaniu poprawnych wskaźników. Być może będę musiał wypróbować różne warianty między wskaźnikami liczbowymi i logicznymi, aby to zoptymalizować.Ważna uwaga: macierz wejściowa powinna być
int32
! Zapisz funkcję w pliku o nazwiehaf.m
Przykładowy skrypt testowy:
Wypróbowałem to za pomocą TIO i MATLAB (nigdy nie instalowałem Octave). Wyobrażam sobie, że uruchomienie go jest tak proste, jak
sudo apt-get install octave
. Polecenieoctave
załaduje graficzny interfejs użytkownika Octave. Jeśli będzie to bardziej skomplikowane, usunę tę odpowiedź, dopóki nie podam bardziej szczegółowych instrukcji instalacji.źródło
Ostatnio Andreas Bjorklund, Brajesh Gupt i ja opublikowaliśmy nowy algorytm dla Hafnians złożonych matryc: https://arxiv.org/pdf/1805.12498.pdf . Dla macierzy n \ razy n skaluje się jak n ^ 3 2 ^ {n / 2}.
Jeśli dobrze rozumiem oryginalny algorytm Andreasa z https://arxiv.org/pdf/1107.4466.pdf , skaluje się jak n ^ 4 2 ^ {n / 2} lub n ^ 3 log (n) 2 ^ {n / 2} jeśli użyłeś transformacji Fouriera do wykonania mnożenia wielomianowego.
Nasz algorytm jest specjalnie opracowany dla złożonych macierzy, więc nie będzie tak szybki, jak te opracowane tutaj dla macierzy {-1,0,1}. Zastanawiam się jednak, czy można skorzystać z niektórych sztuczek użytych do ulepszenia naszej implementacji? Również, jeśli ludzie są zainteresowani, chciałbym zobaczyć, jak działa ich implementacja, gdy podano liczby zespolone zamiast liczb całkowitych. Wreszcie, wszelkie komentarze, krytyka, ulepszenia, błędy, ulepszenia są mile widziane w naszym repozytorium https://github.com/XanaduAI/hafnian/
Twoje zdrowie!
źródło