Encontre o movimento inicial ideal do Chomp

14

Chomp é um jogo para dois jogadores com uma configuração de um retângulo de peças. Cada jogador remove uma peça, juntamente com todas as peças acima e à direita. Quem pega a peça inferior esquerda perde. É fácil provar que o primeiro jogador sempre tem uma jogada vencedora (exceto com um retângulo 1 por 1); encontre.

  1. Entrada é as dimensões do retângulo (dois números)
  2. Saída é o local da jogada vencedora (dois números)
  3. Se houver mais de uma jogada vencedora, você poderá gerar uma delas.

Isso é código de golfe; o código mais curto (qualquer idioma) vence.

Exemplos

Nota: As saídas são apenas os dois números; a arte ASCII abaixo é apenas para demonstrar o que os números significam.

Entrada: 5 3 (índices 1 com base no canto inferior esquerdo)

Saída: 4 3

XXX--
XXXXX
XXXXX

Entrada: 4 4

Saída: 2 2

X---
X---
X---
XXXX

Bônus

Subtraia 15 caracteres da sua pontuação se você exibir todos os movimentos vencedores. Cada par de números deve ser separado por uma nova linha.

Ypnypn
fonte
No seu primeiro exemplo, acho que você tem muitos hífens
kitcar2000
@Kitcar Você está certo; fixo.
Ypnypn
Não consigo entender o formato de saída. Como esses números correspondem a essas posições?
Undergroundmonorail
@undergroundmonorail Índice baseado em 1 no canto inferior esquerdo. o primeiro índice é o eixo horizontal e o segundo é o vertical.
Martin Ender
2
Em resposta à sua recompensa: No xadrez, você tem menos de 119 jogadas possíveis a qualquer momento (geralmente muito menos), e nenhum supercomputador até hoje chegou perto de resolver o xadrez usando até os algoritmos mais conhecidos. Em uma grade de 10 por 10 Chomp, existem 100 possíveis movimentos iniciais e cada um deles tem de 1 a 99 segundos potenciais de movimento. O que faz você pensar que seria fácil a força bruta? Eu recomendo limitar o tamanho da sua grade se você quiser respostas de força bruta. EDIT: Mas não faça isso. Alterar requisitos no meio do concurso é ruim.
Rainbolt

Respostas:

7

GolfScript, 82 ( 108 97 caracteres - 15 bônus)

~),1/{{:F$0=),{F\+}/}%}@(*(0*\{1${1$\{\(@<},=},{1$\{\(@>},+(-!},:Y!{.,/+0}*;}/;Y{.-1=.@?)' '@)n}/

Como eu não conhecia nenhuma heurística, esta solução realiza uma pesquisa exaustiva no espaço da solução. Você pode tentar o código online . Embora a implementação seja muito eficiente, o espaço de pesquisa cresce muito rapidamente com o aumento da entrada.

Exemplos:

> 5 3
4 3

> 5 4
3 3

> 6 6
2 2

Como mencionado acima, a implementação não depende de recursão, mas visita cada nó do espaço de pesquisa apenas uma vez. Abaixo, você encontra uma versão anotada do código que descreve os blocos de construção em mais detalhes.

A representação de uma única placa de tamanho w * h é dada por uma lista de números w no intervalo de 0 a h . Cada número fornece a quantidade de peças na coluna correspondente. Portanto, uma configuração válida é uma lista em que os números não aumentam do início ao fim (com qualquer movimento, você garante que todas as colunas à direita sejam no máximo tão altas quanto a escolhida).

~                   # Evaluate the input (stack is now w h)

# BUILDING THE COMPLETE STATE SPACE
# Iteratively builds the states starting with 1xh board, then 2xh board, ...

),1/                # Generate the array [[0] [1] ... [h]] which is the space for 1xh
{                   # This loop is now ran w-1 times and each run adds all states for the 
                    # board with one additional column
  {                 # The {}/] block simply runs for each of the existing states
    :F$0=           #   Take the smallest entry (which has to be the last one)
    ),              #   For the last column all values 0..x are possible
    {F\+}/          #     Append each of these values to the smaller state
  }%
}@(*

# The order ensures that the less occupied boards are first in the list.
# Thus each game runs from the end of the list (where [h h ... h] is) to 
# the start (where [0 0 ... 0] is located).

# RUN THROUGH THE SEARCH SPACE
# The search algorithm therefore starts with the empty board and works through all
# possible states by simply looping over this list. It builds a list of those states
# which are known as non-winning states, i.e. those states where a player should 
# aim to end after the move

(                   # Skips the empty board (which is a winning configuration)
0*\                 # and makes an empty list out of it (which will be the list of
                    # known non-winning states (initially empty))
{                   # Loop over all possible states
  1$                #   Copy of the list of non-winning states
  {                 #   Filter those which are not reachable from the current state,
                    #   because at least one column has more pieces that the current
                    #   board has
    1$\{\(@<},=
  },
  {                 #   Filter those which are not reachable from the current state,
                    #   because no valid move exists
    1$\{\(@>},+     #     Filter those columns which are different between start and
                    #     end state
    (-!             #     If those columns are all of same height it is possible to move
  },
  :Y                #   Assign the result (list of all non-winning states which are
                    #   reachable from the current configuration within one move)
                    #   to variable Y

  !{                #   If Y is non-empty this one is a winning move, otherwise 
                    #   add it to the list
    .,/+
    0               #     Push dummy value
  }*;
}/
;                   # Discard the list (interesting data was saved to variable Y)

# OUTPUT LOOP
# Since the states were ordered the last one was the starting state. The list of 
# non-winning states were saved to variable Y each time, thus the winning moves 
# from the initial configuration is contained in this variable.

Y{                  # For each item in Y
  .-1=.@?)          #   Get the index (1-based) of the first non-h value
  ' '               #   Append a space
  @)                #   Get the non-h value itself (plus one)
  n                 #   Append a newline
}/
Howard
fonte
+1 Para a própria solução e para o código muito bem comentado
Xuntar
Programação dinâmica de baixo para cima, em vez de cima para baixo. Agradável. Pensei em fazer isso, mas gerar estados em uma ordem válida para travessia de baixo para cima era mais trabalhoso e mais confuso do que a pesquisa recursiva. Estou surpreso que o código tenha acabado por tanto tempo; Eu esperava que uma linguagem concisa como a Golfscript pudesse ter produzido uma solução muito mais curta.
User2357112 suporta Monica
Muito bom e bem pensado
Mouq
8

Python 2 3, 141-15 = 126

def win(x,y):w([y]*x)
w=lambda b,f=print:not[f(r+1,c+1)for r,p in enumerate(b)for c in range(p)if(r+c)*w(b[:r]+[min(i,c)for i in b[r:]],max)]

Pesquisa minimax de força bruta. Para cada jogada possível, vemos recursivamente se o oponente pode vencer depois de fazer essa jogada. Golfe fraco; alguém deve ser capaz de fazer muito melhor. Isso parece um trabalho para a APL.

  • winé a interface pública. Ele pega as dimensões do quadro, converte-o em uma representação do quadro e passa isso paraw .
  • wé o algoritmo minimax. Leva um estado do tabuleiro, tenta todas as jogadas, cria uma lista cujos elementos correspondem às jogadas vencedoras e retorna True se a lista estiver vazia. Com o padrão f=print, a criação da lista tem um efeito colateral de imprimir os movimentos vencedores. O nome da função costumava fazer mais sentido quando ele retornava uma lista de movimentos vencedores, mas depois movi o notna frente da lista para economizar espaço.
  • for r,p in enumerate(b)for c in xrange(p) if(r+c): Itere sobre todos os movimentos possíveis. 1 1é tratado como não uma jogada legal, simplificando um pouco o caso base.
  • b[:r]+[min(i,c)for i in b[r:]]: Construa o estado do quadro após a movimentação representada pelas coordenadas re c.
  • w(b[:r]+[min(i,c)for i in b[r:]],max): Recursão para ver se o novo estado é um estado perdedor. maxé a função mais curta que eu poderia achar que levaria dois argumentos inteiros e não reclamaria.
  • f(r+1,c+1): Se festiver impresso, imprime a movimentação. Seja o que ffor, produz um valor para preencher o comprimento da lista.
  • not [...]: notretorna Truepara listas vazias e Falsenão vazias.

Código original do Python 2, completamente despojado, incluindo memorização para lidar com entradas muito maiores:

def win(x, y):
    for row, column in _win(Board([y]*x)):
        print row+1, column+1

class MemoDict(dict):
    def __init__(self, func):
        self.memofunc = func
    def __missing__(self, key):
        self[key] = retval = self.memofunc(key)
        return retval

def memoize(func):
    return MemoDict(func).__getitem__

def _normalize(state):
    state = tuple(state)
    if 0 in state:
        state = state[:state.index(0)]
    return state

class Board(object):
    def __init__(self, state):
        self.state = _normalize(state)
    def __eq__(self, other):
        if not isinstance(other, Board):
            return NotImplemented
        return self.state == other.state
    def __hash__(self):
        return hash(self.state)
    def after(self, move):
        row, column = move
        newstate = list(self.state)
        for i in xrange(row, len(newstate)):
            newstate[i] = min(newstate[i], column)
        return Board(newstate)
    def moves(self):
        for row, pieces in enumerate(self.state):
            for column in xrange(pieces):
                if (row, column) != (0, 0):
                    yield row, column
    def lost(self):
        return self.state == (1,)

@memoize
def _win(board):
    return [move for move in board.moves() if not _win(board.after(move))]

Demo:

>>> for i in xrange(7, 11):
...     for j in xrange(7, 11):
...         print 'Dimensions: {} by {}'.format(i, j)
...         win(i, j)
...
Dimensions: 7 by 7
2 2
Dimensions: 7 by 8
3 3
Dimensions: 7 by 9
3 4
Dimensions: 7 by 10
2 3
Dimensions: 8 by 7
3 3
Dimensions: 8 by 8
2 2
Dimensions: 8 by 9
6 7
Dimensions: 8 by 10
4 9
5 6
Dimensions: 9 by 7
4 3
Dimensions: 9 by 8
7 6
Dimensions: 9 by 9
2 2
Dimensions: 9 by 10
7 8
9 5
Dimensions: 10 by 7
3 2
Dimensions: 10 by 8
6 5
9 4
Dimensions: 10 by 9
5 9
8 7
Dimensions: 10 by 10
2 2
user2357112 suporta Monica
fonte
Pela 13x13tomada 2x2e você venceria.
precisa saber é o seguinte
@ Davidsbro: Sim, essa é a jogada vencedora para qualquer placa quadrada maior que 1x1, mas meu código ainda não a havia calculado.
User2357112 suporta Monica
2

Perl 6: 113 108 caracteres - 15 = 93 pontos

Este foi difícil! Aqui está a versão não armazenada em cache, que é tecnicamente correta, mas levará muito tempo para entradas não triviais.

sub win(*@b){map ->\i,\j{$(i+1,j+1) if @b[i][j]&&!win @b[^i],@b[i..*].map({[.[^j]]})},(^@b X ^@b[0])[1..*]}

Funciona exatamente como a implementação em Python do @ user2357112 (com o voto positivo , eu não poderia ter descoberto isso sem o trabalho dele / dela!), Exceto que o win () pega uma placa Chomp (matriz) em vez de uma largura e comprimento. Pode ser usado como:

loop {
    my ($y, $x) = get.words;
    .say for @(win [1 xx $x] xx $y)
}

Uma versão com memorização, que pode realmente lidar com entradas decentes (embora não seja otimizada para facilitar a leitura):

my %cache;
sub win (*@b) {
    %cache{
        join 2, map {($^e[$_]??1!!0 for ^@b[0]).join}, @b
    } //= map ->\i,\j{
        $(i+1,j+1) if @b[i][j] and not win
            @b[^i], @b[i..*].map({[.[^(* min j)]]}).grep: +*;
    },(^@b X ^@b[0])[1..*]
}
Mouq
fonte