O desafio é escrever um solucionador para o clássico jogo de lápis e papel Dots and Boxes . Seu código deve ter dois números inteiros m
e n
como entrada, que especifica o tamanho do quadro.
Começando com uma grade vazia de pontos, os jogadores se revezam, adicionando uma única linha horizontal ou vertical entre dois pontos adjacentes não ligados. Um jogador que completa o quarto lado de uma caixa 1 × 1 ganha um ponto e faz outro turno. (Os pontos geralmente são registrados colocando na caixa uma marca de identificação do jogador, como uma inicial). O jogo termina quando não for possível colocar mais linhas. O vencedor do jogo é o jogador com mais pontos.
Você pode assumir que n = m
ou é n = m - 1
e m
é pelo menos 2.
O desafio é chegar ao solve
maior jogo possível de pontos e caixas em menos de um minuto. O tamanho de um jogo é simples n*m
. A saída do seu código deve ser win
, draw
ou lose
qual deve ser o resultado para o primeiro jogador, supondo que os dois jogadores joguem da melhor maneira.
Seu código deve ser compilável / executável no ubuntu usando ferramentas facilmente instaláveis e gratuitas. Relate sua pontuação como a maior área que você pode resolver em seu computador em 1 minuto, juntamente com o tempo. Em seguida, testarei o código no meu computador e criará uma tabela de entradas ordenada por classificação.
No caso de um tie-break, o vencedor será o código mais rápido na maior placa de tamanho que puder resolver em menos de um minuto.
Seria melhor se o código emitido não apenas vencesse ou perdesse, mas também a pontuação real. Isso contribui para uma verificação da sanidade da correção.
Respostas:
C99 - placa 3x3 em 0,084s
Editar: refatorei meu código e fiz uma análise mais profunda dos resultados.
Edições adicionais: Poda adicionada por simetrias. Isso faz 4 configurações de algoritmo: com ou sem simetrias X com ou sem poda alfa-beta
Edições mais distantes: Adicionado memorização usando uma tabela de hash, finalmente alcançando o impossível: resolver uma placa 3x3!
Recursos principais:
#define HASHTABLE_BITWIDTH
). Quando esse tamanho é maior ou igual ao número de paredes, não garante colisões e O (1) é atualizado. As hashtables menores terão mais colisões e serão um pouco mais lentas.-DDEBUG
para impressõesPotenciais melhorias:
corrigir pequeno vazamento de memóriacorrigido na primeira ediçãopoda alfa / betaadicionada na 2ª ediçãoremover simetriasadicionadas na 3ª edição (observe que as simetrias não são tratadas pela memorização, portanto isso permanece uma otimização separada.)memorizaçãoadicionada na 4ª ediçãoCódigo
Devido à falta de organização, o número de arquivos ficou fora de controle. Todo o código foi movido para este repositório do Github . Na edição de memorização, adicionei um script makefile e testing.
Resultados
Notas sobre complexidade
As abordagens de força bruta para pontos e caixas explodem em complexidade muito rapidamente .
Considere um quadro com
R
linhas eC
colunas. ExistemR*C
quadrados,R*(C+1)
paredes verticais eC*(R+1)
paredes horizontais. Isso é um total deW = 2*R*C + R + C
.Como Lembik nos pediu para resolver o jogo com o minimax, precisamos atravessar as folhas da árvore do jogo. Vamos ignorar a poda por enquanto, porque o que importa são ordens de magnitude.
Existem
W
opções para o primeiro movimento. Para cada um deles, o próximo jogador pode jogar qualquer uma dasW-1
paredes restantes, etc. Isso nos dá um espaço de pesquisa deSS = W * (W-1) * (W-2) * ... * 1
, ouSS = W!
. Os fatoriais são enormes, mas isso é apenas o começo.SS
é o número de nós folha no espaço de pesquisa. Mais relevante para nossa análise é o número total de decisões que tiveram que ser tomadas (ou seja, o número de ramosB
na árvore). A primeira camada de ramificações temW
opções. Para cada um deles, o próximo nível temW-1
, etc.Vejamos alguns tamanhos pequenos de tabela:
Esses números estão ficando ridículos. Pelo menos eles explicam por que o código de força bruta parece pendurar para sempre em uma placa 2x3. O espaço de pesquisa de uma placa 2x3 é 742560 vezes maior que 2x2 . Se 2x2 levar 20 segundos para ser concluído, uma extrapolação conservadora prevê mais de 100 dias de tempo de execução para 2x3. Claramente, precisamos podar.
Análise de poda
Comecei adicionando podas muito simples usando o algoritmo alfa-beta. Basicamente, para de procurar se um oponente ideal nunca lhe daria suas oportunidades atuais. "Ei, olhe - eu ganho muito se meu oponente me deixar ganhar todos os quadrados!"
edit Também adicionei poda com base em placas simétricas.
Não uso uma abordagem de memorização, caso algum dia eu adicione memorização e queira manter essa análise separada. Em vez disso,funciona assim: a maioria das linhas tem um "par simétrico" em outro lugar da grade. Existem até 7 simetrias (horizontal, vertical, rotação 180, rotação 90, rotação 270, diagonal e a outra diagonal). Todos os 7 se aplicam a placas quadradas, mas os últimos 4 não se aplicam a placas não quadradas. Cada parede tem um ponteiro para o seu "par" para cada uma dessas simetrias. Se, entrando em um turno, o tabuleiro é horizontalmente simétrico, então apenas um de cada par horizontal precisa ser jogado.editar editar Memoização! Cada parede recebe uma identificação única, que eu convenientemente defini como um indicador; a enésima parede tem o id
1 << n
. O hash de um tabuleiro, então, é apenas o OR de todas as paredes jogadas. Isso é atualizado em cada filial no tempo O (1). O tamanho da hashtable é definido em a#define
. Todos os testes foram executados com tamanho 2 ^ 12, por que não? Quando há mais paredes do que bits indexando a hashtable (12 bits neste caso), os 12 menos significativos são mascarados e usados como índice. As colisões são tratadas com uma lista vinculada em cada índice de hashtable. O gráfico a seguir é minha análise rápida e suja de como o tamanho da hashtable afeta o desempenho. Em um computador com RAM infinita, sempre definiríamos o tamanho da tabela para o número de paredes. Uma placa 3x4 teria uma hashtable 2 ^ 31 de comprimento. Infelizmente, não temos esse luxo.Ok, voltando à poda. Ao interromper a pesquisa no alto da árvore, podemos economizar muito tempo não descendo às folhas. O 'fator de poda' é a fração de todos os ramos possíveis que tivemos que visitar. A força bruta tem um fator de poda de 1. Quanto menor, melhor.
fonte
rows columns
que especificam o tamanho da placaPython - 2x2 em 29s
Postagem cruzada de quebra - cabeças . Não é especialmente otimizado, mas pode ser um ponto de partida útil para outros participantes.
fonte
Javascript - placa 1x2 em 20ms
Demonstração on-line disponível aqui (aviso - muito lento se maior que 1x2 com profundidade total de pesquisa ): https://dl.dropboxusercontent.com/u/141246873/minimax/index.html
Foi desenvolvido para os critérios de vitória originais (código de golfe) e não para velocidade.
Testado no google chrome v35 no windows 7.
fonte