O desafio é escrever o código mais rápido possível para calcular o Hafnian de uma matriz .
O Hafnian de um simétrica 2n
-by- 2n
matriz A
é definida como:
Aqui S 2n representa o conjunto de todas as permutações dos números inteiros de 1
a 2n
, isto é [1, 2n]
.
O link da wikipedia também fornece uma fórmula de aparência diferente que pode ser interessante (e existem métodos ainda mais rápidos se você procurar mais na web). A mesma página da wiki fala sobre matrizes de adjacência, mas seu código também deve funcionar para outras matrizes. Você pode assumir que todos os valores serão inteiros, mas não que todos sejam positivos.
Também existe um algoritmo mais rápido, mas parece difícil de entender. e Christian Sievers foi o primeiro a implementá-lo (em Haskell).
Nesta questão, as matrizes são todas quadradas e simétricas, com dimensão uniforme.
Implementação de referência (observe que este está usando o método mais lento possível).
Aqui está um exemplo de código python do Sr. 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
A tarefa
Você deve escrever código que, dada uma 2n
por 2n
matriz, gera a Hafnian.
Como precisarei testar seu código, seria útil se você desse uma maneira simples de fornecer uma matriz como entrada para seu código, por exemplo, lendo a partir do padrão em. Testarei seu código em matrizes escolhidas aleatoriamente com elementos selecionado de {-1, 0, 1}. O objetivo de testes como esse é reduzir a chance de o Hafnian ser um valor muito grande.
Idealmente, seu código será capaz de ler as matrizes exatamente como as tenho nos exemplos desta pergunta, diretamente do padrão. Essa é a entrada que seria semelhante, [[1,-1],[-1,-1]]
por exemplo. Se você quiser usar outro formato de entrada, pergunte e farei o possível para acomodar.
Pontuações e laços
Testarei seu código em matrizes aleatórias de tamanho crescente e pararei na primeira vez em que o código demorar mais de 1 minuto no meu computador. As matrizes de pontuação serão consistentes para todos os envios, a fim de garantir justiça.
Se duas pessoas obtiverem a mesma pontuação, o vencedor será o mais rápido para esse valor de n
. Se estes estiverem a 1 segundo um do outro, será o primeiro publicado.
Línguas e bibliotecas
Você pode usar qualquer idioma e bibliotecas disponíveis que desejar, mas nenhuma função preexistente para calcular o Hafnian. Sempre que possível, seria bom poder executar seu código; portanto, inclua uma explicação completa de como executar / compilar seu código no Linux, se possível.
Minha máquina Os tempos serão executados na minha máquina de 64 bits. Esta é uma instalação padrão do ubuntu com 8GB de RAM, processador de oito núcleos AMD FX-8350 e Radeon HD 4250. Isso também significa que eu preciso executar seu código.
Solicite respostas em mais idiomas
Seria ótimo obter respostas em sua linguagem de programação super rápida favorita. Para começar, que tal fortran , nim e ferrugem ?
Entre os melhores
- 52 por milhas usando C ++ . 30 segundos.
- 50 por NGN usando C . 50 segundos.
- 46 por Christian Sievers usando Haskell . 40 segundos.
- 40 por milhas usando Python 2 + pypy . 41 segundos.
- 34 por ngn usando Python 3 + pypy . 29 segundos.
- 28 por Dennis usando Python 3 . 35 segundos. (Pypy é mais lento)
Respostas:
Haskell
Isso implementa uma variação do algoritmo 2 de Andreas Björklund: Contando combinações perfeitas tão rápido quanto Ryser .
Compile usando
ghc
opções de tempo de compilação-O3 -threaded
e use opções de tempo de execução+RTS -N
para paralelização. Recebe entrada de stdin.fonte
parallel
evector
deve ser instalado?Python 3
Experimente online!
fonte
C ++ (gcc)
Experimente online! (13s para n = 24)
Baseado na implementação mais rápida do Python no meu outro post. Edite a segunda linha
#define S 3
em sua máquina de 8 núcleos e compile comg++ -pthread -march=native -O2 -ftree-vectorize
.O trabalho divide pela metade, então o valor de
S
deveria serlog2(#threads)
. Os tipos podem ser facilmente alterados entreint
,long
,float
, edouble
modificando o valor de#define TYPE
.fonte
tr -d \ < matrix52.txt > matrix52s.txt
Python 3
Isso calcula haf (A) como uma soma memorizada (A [i] [j] * haf (A sem linhas e colunas iej)).
fonte
C
Outro impl do artigo de Andreas Björklund , que é muito mais fácil de entender se você também olhar o código Haskell de Christian Sievers . Nos primeiros níveis de recursão, ele distribui threads de rodízio nas CPUs disponíveis. O último nível da recursão, que responde por metade das invocações, é otimizado manualmente.
Compilar com:
gcc -O3 -pthread -march=native
; obrigado @Dennis por uma aceleração de 2xn = 24 em 24s no TIO
Algoritmo:
A matriz, que é simétrica, é armazenada na forma triangular inferior esquerda. Os índices de triângulo
i,j
correspondem ao índice linear para oT(max(i,j))+min(i,j)
qualT
é uma macroi*(i-1)/2
. Os elementos da matriz são polinômios de graun
. Um polinômio é representado como uma matriz de coeficientes ordenados do termo constante (p[0]
) até o coeficiente de x n (p[n]
). Os valores iniciais da matriz -1,0,1 são primeiro convertidos em polinômios const.Realizamos uma etapa recursiva com dois argumentos: a meia matriz (ou seja, triângulo)
a
de polinômios e um polinômio separadop
(referido como beta no artigo). Reduzimos om
problema de tamanho (inicialmentem=2*n
) recursivamente para dois problemas de tamanhom-2
e retornamos a diferença de seus hafnianos. Um deles é usar o mesmoa
sem as duas últimas linhas e o mesmop
. Outra é usar o triângulob[i][j] = a[i][j] + shmul(a[m-1][i],a[m-2][j]) + shmul(a[m-1][j],a[m-2][i])
(ondeshmul
é a operação de multiplicação por deslocamento em polinômios - é como um produto polinomial como de costume, adicionalmente multiplicado pela variável "x"; potências maiores que x ^ n são ignoradas) e o polinômio separadoq = p + shmul(p,a[m-1][m-2])
. Quando recursão atinge um tamanho 0a
, voltamos a maior coeficiente de p:p[n]
.A operação de troca e multiplicação é implementada em função
f(x,y,z)
. Ele modificaz
no local. Vagamente falando, é verdadez += shmul(x,y)
. Essa parece ser a parte mais crítica para o desempenho.Após o término da recursão, precisamos corrigir o sinal do resultado multiplicando por (-1) n .
fonte
-march=native
fará uma grande diferença aqui. Pelo menos no TIO, quase reduz o tempo da parede pela metade.Pitão
Essa é uma implementação de referência direta e direta do algoritmo 2 do documento mencionado . As únicas alterações foram manter apenas o valor atual de B , descartando os valores de β atualizando apenas g quando i ∈ X e multiplicando polinômios truncados, calculando apenas os valores até o grau n .
Experimente online!
Aqui está uma versão mais rápida, com algumas das otimizações fáceis.
Experimente online!
Para mais diversão, aqui está uma implementação de referência em J.
fonte
pmul
, usefor j in range(n-i):
e evite oif
j != k
porj < k
. Ele copia uma submatriz no caso else, o que pode ser evitado quando manipulamos e excluímos as duas últimas, em vez das duas primeiras linhas e colunas. E quando computa comx={1,2,4}
e depois comx={1,2,4,6}
, repete seus cálculos atéi=5
. Substituí a estrutura dos dois loops externos com o primeiro loopi
e, em seguida, assumindo recursivamentei in X
ei not in X
. - BTW, pode ser interessante observar o crescimento do tempo necessário em comparação com outros programas mais lentos.Oitava
Esta é basicamente uma cópia da entrada de Dennis , mas otimizada para o Octave. A otimização principal é feita usando a matriz de entrada completa (e sua transposição) e a recursão usando apenas índices da matriz, em vez de criar matrizes reduzidas.
A principal vantagem é a cópia reduzida de matrizes. Embora o Octave não tenha uma diferença entre ponteiros / referências e valores e, funcionalmente, apenas passe por valor, é uma história diferente nos bastidores. Lá, a cópia na gravação (cópia lenta) é usada. Isso significa que, para o código
a=1;b=a;b=b+1
, a variávelb
é copiada apenas para um novo local na última instrução, quando é alterada. Desdematin
ematransp
nunca são alterados, eles nunca serão copiados. A desvantagem é que a função gasta mais tempo calculando os índices corretos. Talvez eu precise tentar diferentes variações entre índices numéricos e lógicos para otimizar isso.Nota importante: a matriz de entrada deve ser
int32
! Salve a função em um arquivo chamadohaf.m
Exemplo de script de teste:
Eu tentei isso usando o TIO e o MATLAB (na verdade, nunca instalei o Octave). Eu imagino fazê-lo funcionar é tão simples quanto
sudo apt-get install octave
. O comandooctave
carregará a GUI do Octave. Se for mais complicado que isso, excluirei esta resposta até fornecer instruções de instalação mais detalhadas.fonte
Recentemente, Andreas Bjorklund, Brajesh Gupt e eu publicamos um novo algoritmo para hafianos de matrizes complexas: https://arxiv.org/pdf/1805.12498.pdf . Para uma matriz n \ times n, ela é dimensionada como n ^ 3 2 ^ {n / 2}.
Se eu entendi corretamente o algoritmo original de Andreas em https://arxiv.org/pdf/1107.4466.pdf, ele é escalado como n ^ 4 2 ^ {n / 2} ou n ^ 3 log (n) 2 ^ {n / 2} se você usou transformadas de Fourier para fazer multiplicações polinomiais.
Nosso algoritmo é especificamente adaptado para matrizes complexas, portanto não será tão rápido quanto o desenvolvido aqui para matrizes {-1,0,1}. Gostaria de saber, no entanto, se alguém pode usar alguns dos truques que você usou para melhorar nossa implementação? Além disso, se as pessoas estiverem interessadas, eu gostaria de ver como é a implementação delas quando recebem números complexos em vez de números inteiros. Finalmente, quaisquer comentários, críticas, melhorias, bugs, melhorias são bem-vindos em nosso repositório https://github.com/XanaduAI/hafnian/
Felicidades!
fonte