Este desafio não é sobre o jogo Snake.
Imagine uma cobra 2D formada desenhando uma linha horizontal de comprimento n
. Em pontos inteiros ao longo de seu corpo, essa cobra pode girar seu corpo em 90 graus. Se definirmos a frente da cobra na extremidade esquerda, a rotação moverá a parte de trás da cobra e a parte da frente permanecerá em pé. Fazendo rotações repetidas, ele pode criar muitas formas diferentes de corpo de cobra.
Regras
- Uma parte do corpo da cobra não pode se sobrepor a outra.
- Tem que ser possível alcançar a orientação final sem que nenhuma parte do corpo da cobra se sobreponha entre elas. Dois pontos que tocam são contados como sobrepostos nesse problema.
- Eu considero uma cobra e seu reverso como tendo a mesma forma.
Tarefa
Até rotação, translação e simetria de espelho, qual é o número total de diferentes formas de corpos de serpentes que podem ser feitas?
Exemplo de rotação de parte do corpo das cobras. Imagine n=10
e a cobra está na orientação inicial de uma linha reta. Agora gire no ponto 4
90 graus no sentido anti-horário. Ficamos com a cobra de 4
que 10
(a cauda da cobra) deitado verticalmente ea cobra a partir 0
de 4
deitado na horizontal. A cobra agora tem um ângulo reto em seu corpo.
Aqui estão alguns exemplos, graças a Martin Büttner.
Começamos com a cobra horizontal.
Agora nós giramos da posição 4.
Terminamos após a rotação nesta orientação.
Agora vamos considerar essa orientação de uma cobra diferente.
Agora podemos ver um movimento ilegal em que haveria uma sobreposição causada durante a rotação.
Ponto
Sua pontuação é a maior n
para a qual seu código pode resolver o problema em menos de um minuto no meu computador.
Quando uma rotação acontece, ela move metade da cobra. Precisamos nos preocupar se alguma parte desta que é girada pode se sobrepor a uma parte da cobra durante a rotação. Para simplificar, podemos assumir que a cobra tem largura zero. Você só pode girar em um ponto específico da cobra até 90 graus no sentido horário ou anti-horário. Pois, você nunca pode dobrar completamente a cobra em duas, pois isso envolveria duas rotações no mesmo ponto na mesma direção.
Formas que não podem ser criadas
Um exemplo simples de uma forma que não pode ser feita é uma capital T
. Uma versão mais sofisticada é
(Obrigado a Harald Hanche-Olsen por este exemplo)
Neste exemplo, todas as linhas horizontais adjacentes estão separadas por 1 e são as verticais. Portanto, não há movimento legal a partir dessa posição e, como o problema é reversível, não há como chegar a partir da posição inicial.
Línguas e bibliotecas
Você pode usar qualquer idioma que tenha um compilador / intérprete disponível gratuitamente / etc. para Linux e quaisquer bibliotecas que também estão disponíveis gratuitamente para Linux.
Minha máquina Os horários serão executados na minha máquina. Esta é uma instalação padrão do ubuntu em um processador AMD FX-8350 de oito núcleos. Isso também significa que eu preciso poder executar seu código. Como conseqüência, use apenas software livre facilmente disponível e inclua instruções completas sobre como compilar e executar seu código.
Respostas:
Python 3 - pontuação provisória: n = 11 (n = 13 com PyPy *)
Como não houve respostas na primeira semana, aqui está um exemplo em Python para incentivar a competição. Tentei torná-lo razoavelmente legível para que as ineficiências possam ser facilmente identificadas para fornecer idéias para outras respostas.
Abordagem
Código
(agora com alguns documentos e declarações após minha primeira tentativa incorreta)
Resultados
Na minha máquina, a cobra mais longa que pode ser calculada em menos de 1 minuto é o comprimento 11 (ou 13 com PyPy *). Obviamente, isso é apenas uma pontuação provisória até descobrirmos qual é a pontuação oficial da máquina de Lembik.
Para comparação, eis os resultados para os primeiros valores de n:
Informe-me se algum deles estiver incorreto.
Se você tiver um exemplo de um arranjo que não possa ser desdobrado, poderá usar a função
neighbours(snake)
para encontrar todos os arranjos alcançáveis em uma etapa, como um teste do código.snake
é uma tupla de direções da junta (0 no sentido horário, 1 no reto, 2 no sentido anti-horário). Por exemplo (1,1,1) é uma cobra reta de comprimento 4 (com 3 articulações).Visualização
Para visualizar uma cobra que tem em mente, ou qualquer uma das cobras retornados por
neighbours
, você pode usar a funçãodisplay(snake)
. Isso aceita uma tupla como as outras funções, mas como não é usada pelo programa principal e, portanto, não precisa ser rápida, também aceita uma string, para sua conveniência.display((1,1,2,0))
é equivalente adisplay("1120")
Como Lembik menciona nos comentários, meus resultados são idênticos ao OEIS A037245, que não leva em consideração as interseções durante a rotação. Isso ocorre porque, para os primeiros valores de n, não há diferença - todas as formas que não se interceptam podem ser alcançadas dobrando uma cobra reta. A correção do código pode ser testada chamando
neighbours()
-se uma cobra que não pode ser desdobrada sem interseção. Como não tem vizinhos, contribuirá apenas para a sequência OEIS, e não para esta. O menor exemplo que conheço é o tamanho 31 da serpente que Lembik mencionou, graças a David K :(1,1,1,1,2,1,2,1,1,1,1,1,1,2,1,1,1,2,1,1,2,2,1,0,1,0,1,1,1,1)
display('111121211111121112112210101111')
fornece a seguinte saída:Eu adoraria ouvir de quem tem um exemplo mais curto sem vizinhos. Suspeito que o menor exemplo desse tipo marque o menor n para o qual as duas seqüências diferem.
* Observe que cada incremento de n leva aproximadamente três vezes mais, portanto, aumentar de n = 11 para n = 13 requer quase 10 vezes o tempo. É por isso que o PyPy permite apenas aumentar n em 2, mesmo que seja consideravelmente mais rápido que o interpretador Python padrão.
fonte
C ++ 11 - quase funcionando :)
Depois de ler este artigo , coletei um pouco de sabedoria daquele cara que aparentemente trabalhou por 25 anos no problema menos complicado de contar caminhos evitáveis em uma treliça quadrada.
Construindo o executável
Compile com eu uso o MinGW no Win7 com g ++ 4.8 para compilações "linux", portanto a portabilidade não é 100% garantida.
g++ -O3 -std=c++11
Também funciona (mais ou menos) com um projeto MSVC2013 padrão
Ao definir indefinidamente
NDEBUG
, você obtém traços de execução de algoritmos e um resumo das configurações encontradas.Performances
com ou sem tabelas de hash, o compilador da Microsoft executa miseravelmente: a compilação g ++ é 3 vezes mais rápida .
O algoritmo praticamente não usa memória.
Como a verificação de colisão é aproximadamente em O (n), o tempo de computação deve ser em O (nk n ), com k ligeiramente menor que 3.
No meu [email protected], n = 17 leva cerca de 1:30 (cerca de 2 milhões cobras / minuto).
Não terminei de otimizar, mas não esperaria mais do que um ganho de x3, portanto, basicamente, espero alcançar talvez n = 20 em uma hora ou n = 24 em um dia.
Atingir a primeira forma inconstante conhecida (n = 31) levaria entre alguns anos e uma década, assumindo que não houvesse falta de energia.
Contando formas
Uma cobra de tamanho N tem juntas N-1 .
Cada articulação pode ser deixada reta ou dobrada para a esquerda ou direita (3 possibilidades).
O número de possíveis dobras é, portanto, 3 N-1 .
As colisões reduzirão um pouco esse número, de modo que o número real esteja próximo de 2,7 N-1
No entanto, muitas dessas dobras levam a formas idênticas.
duas formas são idênticas se houver uma rotação ou simetria que possa transformar uma na outra.
Vamos definir um segmento como qualquer parte reta do corpo dobrado.
Por exemplo, uma cobra de tamanho 5 dobrada na 2ª junta teria 2 segmentos (um com 2 unidades de comprimento e o segundo com 3 unidades).
O primeiro segmento será nomeado como cabeça e a última cauda .
Por convenção, orientamos a cabeça das cobras horizontalmente com o corpo apontando para a direita (como na primeira figura do OP).
Designamos uma figura com uma lista de comprimentos de segmentos assinados, com comprimentos positivos indicando uma dobra à direita e negativos uma dobra à esquerda.
O comprimento inicial é positivo por convenção.
Separando segmentos e dobras
Se considerarmos apenas as diferentes maneiras pelas quais uma cobra de comprimento N pode ser dividida em segmentos, terminamos com uma repartição idêntica às composições de N.
Usando o mesmo algoritmo mostrado na página da wiki, é fácil gerar todas as 2 partições N-1 possíveis da cobra.
Cada partição, por sua vez, gera todas as dobras possíveis aplicando dobras à esquerda ou à direita em todas as suas juntas. Uma dessas dobras será chamada de configuração .
Todas as partições possíveis podem ser representadas por um número inteiro de bits N-1, onde cada bit representa a presença de uma junta. Vamos chamar esse número inteiro de gerador .
Partições de poda
Observando que dobrar uma partição da cabeça para baixo é equivalente a dobrar a partição simétrica da cauda para cima, podemos encontrar todos os pares de partições simétricas e eliminar uma em duas.
O gerador de uma partição simétrica é o gerador da partição escrito em ordem de bits reversa, o que é trivialmente fácil e barato de detectar.
Isso eliminará quase metade das partições possíveis, com exceção das partições com geradores "palíndricos" que são mantidos inalterados pela reversão de bits (por exemplo, 00100100).
Cuidando de simetrias horizontais
Com nossas convenções (uma cobra começa a apontar para a direita), a primeira dobra aplicada à direita produzirá uma família de dobras que serão simétricas horizontais daquelas que diferem apenas na primeira dobra.
Se decidirmos que a primeira curva sempre estará à direita, eliminaremos toda a simetria horizontal de uma só vez.
Limpando os palíndromos
Esses dois cortes são eficientes, mas não o suficiente para cuidar desses palíndromos traquinas.
A verificação mais completa no caso geral é a seguinte:
considere uma configuração C com uma partição palindrômica.
Poderíamos verificar todas as novas configurações contra as outras 3. No entanto, como já geramos apenas configurações começando com uma curva à direita, temos apenas uma simetria possível para verificar:
Essa é a única configuração que podemos duplicar.
Eliminando duplicatas sem armazenamento
Minha abordagem inicial foi armazenar todas as configurações em uma enorme tabela de hash, para eliminar duplicatas, verificando a presença de uma configuração simétrica previamente calculada.
Graças ao artigo acima mencionado, ficou claro que, como as partições e as dobras são armazenadas como campos de bits, elas podem ser comparadas como qualquer valor numérico.
Portanto, para eliminar um membro de um par simétrico, você pode simplesmente comparar os dois elementos e manter sistematicamente o menor (ou o maior, como desejar).
Assim, testar uma configuração para duplicação equivale a calcular a partição simétrica e, se ambas são idênticas, a dobra. Nenhuma memória é necessária.
Ordem de geração
Claramente, a verificação de colisão será a parte que mais consome tempo; portanto, reduzir esses cálculos é uma grande economia de tempo.
Uma solução possível é ter uma "cobra ragdoll" que começará em uma configuração plana e será dobrada gradualmente, para evitar recalcular toda a geometria da cobra para cada configuração possível.
Ao escolher a ordem na qual as configurações são testadas, para que no máximo um boneco de pano seja armazenado para cada número total de juntas, podemos limitar o número de instâncias a N-1.
Eu uso uma varredura recursiva do saquê da cauda para baixo, adicionando uma única junta em cada nível. Assim, uma nova instância de ragdoll é construída sobre a configuração pai, com uma única curva adicional.
Isso significa que as dobras são aplicadas em uma ordem seqüencial, o que parece ser suficiente para evitar auto-colisões em quase todos os casos.
Quando a autolisão é detectada, as dobras que levam ao movimento ofensivo são aplicadas em todas as ordens possíveis até que a dobra legítima seja encontrada ou todas as combinações sejam esgotadas.
Verificação estática
Antes de pensar em partes móveis, achei mais eficiente testar a forma final estática de uma cobra para auto-interseções.
Isso é feito desenhando a cobra em uma grade. Cada ponto possível é plotado da cabeça para baixo. Se houver uma auto-interseção, pelo menos um par de pontos cairá no mesmo local. Isso requer exatamente N plotagens para qualquer configuração de cobra, por um tempo O (N) constante.
A principal vantagem dessa abordagem é que apenas o teste estático simplesmente seleciona caminhos válidos de auto-evitação em uma treliça quadrada, o que permite testar todo o algoritmo inibindo a detecção dinâmica de colisões e assegurando que encontramos a contagem correta desses caminhos.
Verificação dinâmica
Quando uma cobra se dobra em torno de uma junta, cada segmento girado varre uma área cuja forma é qualquer coisa, menos trivial.
Claramente, você pode verificar colisões testando a inclusão em todas essas áreas varridas individualmente. Uma verificação global seria mais eficiente, mas, dada a complexidade das áreas, não consigo pensar em nenhuma delas (exceto talvez usar uma GPU para desenhar todas as áreas e executar uma verificação de ocorrência global).
Como o teste estático cuida das posições inicial e final de cada segmento, basta verificar as interseções com os arcos varridos por cada segmento rotativo.
Após uma discussão interessante com o trichoplax e um pouco de JavaScript para me orientar, criei este método:
Para tentar colocar em poucas palavras, se você chamar
(fonte: free.fr )
Para qualquer segmento que não contenha I , a área varrida é vinculada por 2 arcos (e 2 segmentos já atendidos pela verificação estática).
Se eu cair dentro do segmento, o arco varrido por eu também deverá ser levado em consideração.
Isso significa que podemos verificar cada segmento imóvel em relação a cada segmento rotativo com 2 ou 3 interseções segmento-arco
Usei a geometria vetorial para evitar completamente as funções trigonométricas.
As operações de vetor produzem código compacto e (relativamente) legível.
A interseção segmento a arco requer um vetor de ponto flutuante, mas a lógica deve ser imune a erros de arredondamento.
Encontrei esta solução elegante e eficiente em um post obscuro no fórum. Eu me pergunto por que não é mais amplamente divulgado.
Funciona?
Inibir a detecção dinâmica de colisão produz os caminhos corretos de auto-evitação, que contam até n = 19, por isso estou bastante confiante de que o layout global funcione.
A detecção dinâmica de colisão produz resultados consistentes, embora a verificação de dobras em ordem diferente esteja ausente (por enquanto).
Como conseqüência, o programa conta cobras que podem ser dobradas da cabeça para baixo (ou seja, com as juntas dobradas em ordem crescente da distância da cabeça).
fonte