Trova la mossa iniziale ottimale di Chomp


14

Chomp è un gioco a due giocatori con una configurazione di un rettangolo di pezzi. Ogni giocatore a turno rimuove qualsiasi pezzo, insieme a tutti i pezzi sopra di esso e alla sua destra. Chi prende il pezzo in basso a sinistra perde. Si può dimostrare abbastanza facilmente che il primo giocatore ha sempre una mossa vincente (tranne che con un rettangolo 1 per 1); Trovalo.

  1. L'input è le dimensioni del rettangolo (due numeri)
  2. L'output è la posizione della mossa vincente (due numeri)
  3. Se c'è più di una mossa vincente, puoi emetterne una qualsiasi.

Questo è il codice golf; vince il codice più breve (qualsiasi lingua).

Esempi

Nota: le uscite sono solo i due numeri; l'arte ASCII sotto è solo per dimostrare cosa significano i numeri.

Input: 5 3 (indici basati su 1 a partire dall'angolo in basso a sinistra)

Uscita: 4 3

XXX--
XXXXX
XXXXX

Ingresso: 4 4

Uscita: 2 2

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

indennità

Sottrai 15 caratteri dal tuo punteggio se ottieni tutte le mosse vincenti. Ogni coppia di numeri deve essere separata da una nuova riga.


Nel tuo primo esempio, penso che tu abbia troppi trattini
kitcar2000

@Kitcar Hai ragione; fisso.
Ypnypn,

Non riesco a capire il formato di output. In che modo quei numeri corrispondono a quelle posizioni?
undergroundmonorail

@undergroundmonorail 1 indice basato in basso a sinistra. il primo indice è l'asse orizzontale e il secondo indice è l'indice verticale.
Martin Ender,

2
In risposta alla tua generosità: in Chess, hai meno di 119 mosse possibili in un dato momento (di solito molto meno), e nessun supercomputer fino ad oggi si è avvicinato alla risoluzione degli scacchi usando persino gli algoritmi più noti. Su una griglia Chomp 10 per 10, ci sono 100 possibili prime mosse e ognuna di queste ha 1-99 seconde mosse potenziali. Cosa ti fa pensare che sarebbe facile forzare la forza? Ti consiglio di limitare le dimensioni della griglia se desideri risposte a forza bruta. EDIT: Ma non farlo. La modifica dei requisiti a metà gara è negativa.
Rainbolt,

Risposte:


7

GolfScript, 82 ( 108 97 caratteri - 15 bonus)

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

Poiché non conoscevo alcuna euristica, questa soluzione esegue una ricerca esaustiva nello spazio della soluzione. Puoi provare il codice online . Sebbene l'implementazione sia molto efficiente, lo spazio di ricerca cresce molto rapidamente con l'aumentare dell'input.

Esempi:

> 5 3
4 3

> 5 4
3 3

> 6 6
2 2

Come accennato in precedenza, l'implementazione non si basa sulla ricorsione, ma visita ogni nodo dello spazio di ricerca una sola volta. Di seguito puoi trovare una versione annotata del codice che descrive i blocchi costitutivi in ​​modo più dettagliato.

La rappresentazione di una singola scheda di dimensioni w * h è data da un elenco di numeri w nell'intervallo da 0 a h . Ogni numero indica la quantità di pezzi nella colonna corrispondente. Pertanto, una configurazione valida è un elenco in cui i numeri non aumentano dall'inizio alla fine (con qualsiasi mossa si garantisce che tutte le colonne a destra siano al massimo alte quanto quella scelta).

~                   # 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
}/

+1 Per la soluzione stessa e per il codice molto ben commentato
Xuntar

Programmazione dinamica bottom-up, piuttosto che top-down. Bello. Ho pensato di farlo, ma generare stati in un ordine valido per l'attraversamento dal basso verso l'alto era più lavoro e più confusione della ricerca ricorsiva. Sono sorpreso che il codice sia finito così a lungo; Mi aspettavo che un linguaggio conciso come Golfscript avrebbe potuto produrre una soluzione molto più breve.
user2357112 supporta Monica

Molto bello e ben pensato
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)]

Ricerca minimax forza bruta. Per ogni possibile mossa, vediamo ricorsivamente se l'avversario può vincere dopo aver fatto quella mossa. Abbastanza debolmente golfato; qualcun altro dovrebbe essere in grado di fare molto meglio. Sembra un lavoro per APL.

  • winè l'interfaccia pubblica. Prende le dimensioni della scheda, la converte in una rappresentazione della scheda e la passa a w.
  • wè l'algoritmo minimax. Prende uno stato di board, prova tutte le mosse, crea un elenco i cui elementi corrispondono alle mosse vincenti e restituisce True se l'elenco è vuoto. Con l'impostazione predefinita f=print, la creazione dell'elenco ha un effetto collaterale nella stampa delle mosse vincenti. Il nome della funzione usato aveva più senso quando restituiva un elenco di mosse vincenti, ma poi ho spostato il notdavanti all'elenco per salvare uno spazio.
  • for r,p in enumerate(b)for c in xrange(p) if(r+c): Scorrere su tutte le mosse possibili. 1 1viene considerato non una mossa legale, semplificando un po 'il caso di base.
  • b[:r]+[min(i,c)for i in b[r:]]: Costruisci lo stato del tabellone dopo lo spostamento rappresentato da coordinate re c.
  • w(b[:r]+[min(i,c)for i in b[r:]],max): Ricerchi per vedere se il nuovo stato è in perdita. maxè la funzione più breve che ho trovato che richiederebbe due argomenti interi e non si lamenterebbe.
  • f(r+1,c+1): Se fè stampa, stampa lo spostamento. Qualunque cosa fsia, produce un valore per riempire la lunghezza dell'elenco.
  • not [...]: notritorna Trueper liste vuote e Falsenon vuote .

Codice Python 2 originale, completamente non modificato, inclusa la memoizzazione per gestire input molto più grandi:

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

Per il 13x13take 2x2e vinceresti.
davidsbro,

@davidsbro: Sì, questa è la mossa vincente per qualsiasi board quadrato più grande di 1x1, ma il mio codice non l'aveva ancora calcolato.
user2357112 supporta Monica

2

Perl 6: 113 108 caratteri - 15 = 93 punti

Questo è stato difficile! Ecco la versione non cache, che è tecnicamente corretta ma richiederà molto tempo per input non banali.

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..*]}

Funziona proprio come l' implementazione di Python di @utente2357112 (votalo , non avrei potuto capirlo senza il suo lavoro!) Tranne per il fatto che win () prende una scheda Chomp (array) invece di una larghezza e lunghezza. Può essere usato come:

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

Una versione con memoization, che può effettivamente gestire input decenti (non ottimizzato per la leggibilità, però):

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..*]
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.