Encuentra el movimiento inicial óptimo de Chomp

14

Chomp es un juego de dos jugadores con una configuración de un rectángulo de piezas. Cada jugador toma un turno para eliminar cualquier pieza, junto con todas las piezas que se encuentran arriba y a su derecha. Quien tome la pieza inferior izquierda pierde. Se puede demostrar con bastante facilidad que el primer jugador siempre tiene un movimiento ganador (excepto con un rectángulo de 1 por 1); Encuéntralo.

  1. La entrada es las dimensiones del rectángulo (dos números)
  2. La salida es la ubicación del movimiento ganador (dos números)
  3. Si hay más de un movimiento ganador, entonces puedes sacar cualquiera de ellos.

Este es el código de golf; el código más corto (cualquier idioma) gana.

Ejemplos

Nota: Las salidas son solo los dos números; El siguiente arte ASCII es solo para demostrar lo que significan los números.

Entrada: 5 3 (índices basados ​​en 1 a partir de la esquina inferior izquierda)

Salida: 4 3

XXX--
XXXXX
XXXXX

Entrada: 4 4

Salida: 2 2

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

Prima

Resta 15 personajes de tu puntuación si sacas todos los movimientos ganadores. Cada par de números debe estar separado por una nueva línea.

Ypnypn
fuente
En su primer ejemplo, creo que tiene demasiados guiones
kitcar2000 el
@Kitcar Tienes razón; fijo.
Ypnypn
No puedo entender el formato de salida. ¿Cómo corresponden esos números a esas posiciones?
undergroundmonorail
@undergroundmonorail 1 índice basado en la parte inferior izquierda. el primer índice es el eje horizontal y el segundo índice es el índice vertical.
Martin Ender
2
En respuesta a su recompensa: en Ajedrez, tiene menos de 119 movimientos posibles en un momento dado (generalmente mucho menos), y ninguna supercomputadora hasta el día de hoy se ha acercado a resolver Ajedrez utilizando incluso los algoritmos más conocidos. En una cuadrícula de 10 por 10 Chomp, hay 100 primeros movimientos posibles, y cada uno de ellos tiene 1-99 segundos movimientos potenciales. ¿Qué te hace pensar que sería fácil para la fuerza bruta? Recomiendo limitar el tamaño de su cuadrícula si desea respuestas de fuerza bruta. EDITAR: Pero no hagas eso. Cambiar los requisitos a mitad del concurso es malo.
Rainbolt

Respuestas:

7

GolfScript, 82 ( 108 97 caracteres - 15 bonus)

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

Como no conocía ninguna heurística, esta solución realiza una búsqueda exhaustiva en el espacio de la solución. Puede probar el código en línea . Aunque la implementación es muy eficiente, el espacio de búsqueda crece muy rápido al aumentar la entrada.

Ejemplos:

> 5 3
4 3

> 5 4
3 3

> 6 6
2 2

Como se mencionó anteriormente, la implementación no se basa en la recursividad, sino que visita cada nodo del espacio de búsqueda solo una vez. A continuación puede encontrar una versión anotada del código que describe los bloques de construcción con más detalle.

La representación de una sola placa de tamaño w * h viene dada por una lista de números w en el rango de 0 a h . Cada número da la cantidad de piezas en la columna correspondiente. Por lo tanto, una configuración válida es una lista donde los números no aumentan de principio a fin (con cualquier movimiento, se asegura de que todas las columnas a la derecha sean tan altas como la elegida).

~                   # 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
fuente
+1 Para la solución en sí y para el código muy bien comentado
Xuntar
Programación dinámica ascendente, en lugar de descendente. Agradable. Pensé en hacerlo, pero generar estados en un orden válido para el recorrido de abajo hacia arriba era más trabajo y más confuso que la búsqueda recursiva. Me sorprende que el código haya terminado tanto tiempo; Esperaba que un lenguaje breve como Golfscript pudiera haber producido una solución mucho más corta.
user2357112 es compatible con Monica
Muy agradable y bien pensado
Mouq
8

Pitón 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)]

Búsqueda minimax de fuerza bruta. Para cada movimiento posible, vemos recursivamente si el oponente puede ganar después de que hacemos ese movimiento. Bastante débilmente golfizado; alguien más debería poder hacerlo mucho mejor. Esto se siente como un trabajo para APL.

  • wines la interfaz pública Toma las dimensiones del tablero, lo convierte en una representación del tablero y se lo pasa a w.
  • wes el algoritmo minimax Toma un estado de tablero, intenta todos los movimientos, construye una lista cuyos elementos corresponden a movimientos ganadores y devuelve True si la lista está vacía. Con el valor predeterminado f=print, la creación de la lista tiene el efecto secundario de imprimir los movimientos ganadores. El nombre de la función solía tener más sentido cuando devolvía una lista de movimientos ganadores, pero luego moví el notfrente de la lista para ahorrar un espacio.
  • for r,p in enumerate(b)for c in xrange(p) if(r+c): Iterar sobre todos los movimientos posibles. 1 1se trata como un movimiento no legal, simplificando un poco el caso base.
  • b[:r]+[min(i,c)for i in b[r:]]: Construye el estado del tablero después del movimiento representado por las coordenadas ry c.
  • w(b[:r]+[min(i,c)for i in b[r:]],max): Recurse para ver si el nuevo estado es un estado perdedor. maxes la función más corta que pude encontrar que tomaría dos argumentos enteros y no se quejaría.
  • f(r+1,c+1): Si fes imprimir, imprime el movimiento. Sea lo que fsea, produce un valor para rellenar la longitud de la lista.
  • not [...]: notdevuelve Truepara listas vacías y Falsepara no vacías.

Código original de Python 2, completamente sin golf, incluida la memorización para manejar entradas mucho más grandes:

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))]

Manifestación:

>>> 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 es compatible con Monica
fuente
Por la 13x13toma 2x2y ganarías.
davidsbro
@davidsbro: Sí, ese es el movimiento ganador para cualquier tablero cuadrado más grande que 1x1, pero mi código aún no lo había calculado.
user2357112 es compatible con Monica
2

Perl 6: 113108 caracteres - 15 = 93 puntos

Este fue duro! Aquí está la versión en caché, que es técnicamente correcto, pero tomará un muy largo tiempo para las entradas no triviales.

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 igual que la implementación de Python de @ user2357112 (¡lo votó , no podría haberlo resuelto sin su trabajo!) Excepto que win () toma un tablero (matriz) de Chomp en lugar de un ancho y una longitud. Se puede usar como:

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

Una versión con memorización, que en realidad puede manejar entradas decentes (sin embargo, no está optimizada para facilitar la lectura):

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
fuente