Come trovare il numero minimo di mosse per spostare un oggetto in una posizione in una pila?


12

Stacks

Dato un set di stack NXP con N come numero di stack e P come capacità di stack, come posso calcolare il numero minimo di swap necessari per spostarmi da un nodo nella posizione A a una posizione arbitraria B? Sto progettando un gioco e l'obiettivo finale è quello di ordinare tutte le pile in modo che siano tutte dello stesso colore.

# Let "-" represent blank spaces, and assume the stacks are
stacks = [
           ['R', 'R', 'R', 'R'], 
           ['Y', 'Y', 'Y', 'Y'], 
           ['G', 'G', 'G', 'G'], 
           ['-', '-', '-', 'B'], 
           ['-', 'B', 'B', 'B']
         ]

Se voglio inserire una "B" in stacks[1][1]tal senso stacks[1] = ["-", "B", "Y", "Y"]. Come posso determinare il numero minimo di mosse necessarie per farlo?

Ho esaminato diversi approcci, ho provato algoritmi genetici che generano tutte le possibili mosse da uno stato, li segnano e quindi continuano lungo i migliori percorsi di punteggio, ho anche cercato di eseguire l'algoritmo di Djikstra per l'individuazione del percorso sul problema . Sembra frustrantemente semplice, ma non riesco a trovare un modo per farlo funzionare in qualcosa di diverso dal tempo esponenziale. C'è un algoritmo che mi manca applicabile qui?

modificare

Ho scritto questa funzione per calcolare il numero minimo di mosse richieste: pile: Elenco di Elenco dei personaggi che rappresentano i pezzi nella pila, pile [0] [0] è il primo della pila [0] stack_ind: l'indice del pila che il pezzo verrà aggiunto a needs_piece: il pezzo che dovrebbe essere aggiunto alla pila needs_index: l'indice in cui dovrebbe essere posizionato il pezzo

def calculate_min_moves(stacks, stack_ind, needs_piece, needs_index):
    # Minimum moves needed to empty the stack that will receive the piece so that it can hold the piece
    num_removals = 0
    for s in stacks[stack_ind][:needs_index+1]:
        if item != "-":
            num_removals += 1

    min_to_unlock = 1000
    unlock_from = -1
    for i, stack in enumerate(stacks):
        if i != stack_ind:
            for k, piece in enumerate(stack):
                if piece == needs_piece:
                    if k < min_to_unlock:
                        min_to_unlock = k
                        unlock_from = i

    num_free_spaces = 0
    free_space_map = {}

    for i, stack in enumerate(stacks):
        if i != stack_ind and i != unlock_from:
            c = stack.count("-")
            num_free_spaces += c
            free_space_map[i] = c

    if num_removals + min_to_unlock <= num_free_spaces:
        print("No shuffling needed, there's enough free space to move all the extra nodes out of the way")
    else:
        # HERE
        print("case 2, things need shuffled")

Modifica: Casi di prova su pile:

stacks = [
           ['R', 'R', 'R', 'R'], 
           ['Y', 'Y', 'Y', 'Y'], 
           ['G', 'G', 'G', 'G'], 
           ['-', '-', '-', 'B'], 
           ['-', 'B', 'B', 'B']
         ]

Case 1: stacks[4][1] should be 'G'
Move 'B' from stacks[4][1] to stacks[3][2]
Move 'G' from stacks[2][0] to stacks[4][1]
num_removals = 0 # 'G' is directly accessible as the top of stack 2
min_to_unlock = 1 # stack 4 has 1 piece that needs removed
free_spaces = 3 # stack 3 has free spaces and no pieces need moved to or from it
moves = [[4, 3], [2, 4]]
min_moves = 2
# This is easy to calculate
Case 2: stacks[0][3] should be 'B'
Move 'B' from stacks[3][3] to stack[4][0]
Move 'R' from stacks[0][0] to stacks[3][3]
Move 'R' from stacks[0][1] to stacks[3][2]
Move 'R' from stacks[0][2] to stacks[3][1]
Move 'R' from stacks[0][3] to stacks[3][0]
Move 'B' from stacks[4][0] to stacks[0][3]
num_removals = 0 # 'B' is directly accessible 
min_to_unlock = 4 # stack 0 has 4 pieces that need removed
free_spaces = 3 # If stack 3 and 4 were switched this would be 1
moves = [[3, 4], [0, 3], [0, 3], [0, 3], [0, 3], [4, 0]]
min_moves = 6
#This is hard to calculate

L'implementazione del codice attuale non è la parte difficile, sta determinando come implementare un algoritmo che risolva il problema con cui sto lottando.

Come da richiesta di @ YonIif, ho creato una sintesi per il problema.

Quando viene eseguito, genera una matrice casuale delle pile e sceglie un pezzo casuale che deve essere inserito in una pila casuale in una posizione casuale.

Eseguendolo stampa qualcosa di questo formato sulla console.

All Stacks: [['-', '-', 'O', 'Y'], ['-', 'P', 'P', 'O'], ['-', 'P', 'O', 'Y'], ['Y', 'Y', 'O', 'P']]
Stack 0 is currently ['-', '-', 'O', 'Y']
Stack 0 should be ['-', '-', '-', 'P']

Aggiornamento di stato

Sono molto determinato a risolvere questo problema in qualche modo .

Tieni presente che ci sono modi per ridurre al minimo il numero di casi, come quelli di @Hans Olsson citati nei commenti. Il mio approccio più recente a questo problema è stato quello di sviluppare un insieme di regole simili a quelle menzionate e di impiegarle in un algoritmo generazionale.

Regole come:

Non invertire mai una mossa. Vai da 1> 0 quindi 0-> 1 (non ha senso)

Non spostare mai un pezzo due volte di seguito. Non passare mai da 0 -> 1 quindi 1 -> 3

Dato uno spostamento dalle pile [X] alle pile [Y], quindi un certo numero di mosse, quindi un passaggio dalle pile [Y] alle pile [Z], se le pile [Z] si trovano nello stesso stato in cui si trovava quando la mossa dalle pile [X] alle pile [Y], una mossa avrebbe potuto essere eliminata spostandosi dalle pile [X] direttamente alle pile [Z]

Attualmente, sto affrontando questo problema con un tentativo di creare abbastanza regole, che minimizzi il numero di mosse "valide", abbastanza da poter calcolare una risposta usando un algoritmo generazionale. Se qualcuno può pensare a regole aggiuntive, sarei interessato a sentirle nei commenti.

Aggiornare

Grazie alla risposta di @RootTwo ho avuto una svolta, che tratterò qui.

Sulla svolta

Definire l'altezza della porta come la profondità in cui deve essere posizionato il goal goal nella pila di destinazione.

Ogni volta che un goal goal viene posizionato all'indice <= stack_height - altezza goal, ci sarà sempre un percorso più breve verso la vittoria tramite il metodo clear_path ().

Let S represent some solid Piece.

IE

Stacks = [ [R, R, G], [G, G, R], [-, -, -] ]
Goal = Stacks[0][2] = R
Goal Height = 2.
Stack Height - Goal Height = 0

Dato un po 'di stack tale stack[0] = R, il gioco è vinto.

                       GOAL
[ [ (S | -), (S | -), (S | -) ], [R, S, S], [(S | - ), (S | -), (S | -)] ]

Poiché è noto che sono sempre disponibili almeno gli spazi vuoti stack_height, il caso peggiore sarebbe:

 [ [ S, S, !Goal ], [R, S, S], [-, -, -]

Poiché sappiamo che il goal goal non può essere nella destinazione goal o il gioco è vinto. Nel qual caso il numero minimo di mosse richieste sarebbero le mosse:

(0, 2), (0, 2), (0, 2), (1, 0)

Stacks = [ [R, G, G], [-, R, R], [-, -, G] ]
Goal = Stack[0][1] = R
Stack Height - Goal Height = 1

Dato un po 'di stack tale stack[1] = R, il gioco è vinto.

              GOAL
[ [ (S | -), (S | -), S], [ (S | -), R, S], [(S | -), (S | -), (S | -)]

Sappiamo che ci sono almeno 3 spazi vuoti disponibili, quindi il caso peggiore sarebbe:

[ [ S, !Goal, S], [S, R, S], [ -, -, - ]

In questo caso il numero minimo di mosse sarebbero le mosse:

(1, 2), (0, 2), (0, 2), (1, 0)

Questo vale per tutti i casi.

Pertanto, il problema è stato ridotto al problema di trovare il numero minimo di mosse richieste per posizionare il goal goal all'altezza del goal o sopra.

Ciò divide il problema in una serie di sotto-problemi:

  1. Quando lo stack di destinazione ha il suo pezzo accessibile! = Goal goal, determinando se esiste una posizione valida per quel pezzo, o se il pezzo deve rimanere lì mentre un altro pezzo viene scambiato.

  2. Quando lo stack di destinazione ha il suo pezzo accessibile == goal goal, determinando se può essere rimosso e posizionato all'altezza obiettivo desiderata, o se il pezzo deve rimanere mentre un altro viene scambiato.

  3. Quando i due casi precedenti richiedono un altro pezzo da scambiare, determinare quali pezzi scambiare per aumentare per consentire al pezzo da raggiungere di raggiungere l'altezza.

Lo stack di destinazione deve sempre valutare prima i casi.

IE

stacks = [ [-, R, G], [-, R, G], [-, R, G] ]

Goal = stacks[0][1] = G

Il primo controllo dello stack degli obiettivi porta a:

(0, 1), (0, 2), (1, 0), (2, 0) = 4 Moves

Ignorare lo stack degli obiettivi:

(1, 0), (1, 2), (0, 1), (0, 1), (2, 0) = 5 Moves

2
Hai provato A * ? È abbastanza simile all'algoritmo di Dijkstra ma a volte è considerevolmente più veloce.
Yonlif,

1
Potete per favore condividere un link repo github? Vorrei sperimentare me stesso se va bene. @Tristen
Yonlif,

1
Dopo una prima occhiata, questo problema sembra NP-difficile. Probabilmente non è all'interno di NP (non NP-completo), perché anche se ti do una soluzione ottimale, non puoi nemmeno verificarlo facilmente. Ciò è noto per problemi di ottimizzazione delle permutazioni. Suggerirei di postare il problema in CS . Cerca negli algoritmi di approssimazione per questo problema. Questo è un problema piuttosto difficile ma dovrebbe esistere un'approssimazione decente. Questo è simile: Arbitrary Towers of Hanoi
DarioHett,

1
@DarioHett Era quello di cui ero preoccupato! Incrociai le dita e non sarebbe stato un problema NP-Hard, ma avevo anche la sensazione che potesse esserlo. Ho avuto più fortuna con un algoritmo genetico e anche alcune funzioni di punteggio specializzate che segnano le mosse. Dò un'occhiata alle Torri arbitrarie di Hanoi! Grazie per il suggerimento
Tristen,

1
Se provi a generare il puzzle in modo casuale, ricorda di rimuovere le mosse ovviamente ridondanti (spostare qualcosa indietro dopo una mossa in avanti o fare una mossa in due passaggi quando uno sarebbe sufficiente; e anche in combinazione con mosse possibilmente non correlate mescolate).
Hans Olsson,

Risposte:


1

Ho trovato due opzioni, ma nessuna di esse è in grado di risolvere il caso 2 in modo tempestivo. La prima opzione utilizza A * con una misura della distanza della stringa come h (n), la seconda opzione è IDA *. Ho testato molte misure di somiglianza delle stringhe, ho usato smith-waterman nel mio approccio. Ho cambiato la tua notazione per trattare il problema più velocemente. Ho aggiunto numeri alla fine di ogni cifra per verificare se un pezzo è stato spostato due volte.

Ecco i casi su cui ho testato:

start = [
 ['R1', 'R2', 'R3', 'R4'], 
 ['Y1', 'Y2', 'Y3', 'Y4'], 
 ['G1', 'G2', 'G3', 'G4'], 
 ['B1'], 
 ['B2', 'B3', 'B4']
]

case_easy = [
 ['R', 'R', 'R', 'R'], 
 ['Y', 'Y', 'Y', 'Y'], 
 ['G', 'G', 'G'], 
 ['B', 'B'], 
 ['B', 'B', 'G']
]


case_medium = [
 ['R', 'R', 'R', 'R'], 
 ['Y', 'Y', 'Y', 'B'], 
 ['G', 'G', 'G'], 
 ['B'],
 ['B', 'B', 'G', 'Y']
]

case_medium2 = [
 ['R', 'R', 'R' ], 
 ['Y', 'Y', 'Y', 'B'], 
 ['G', 'G' ], 
 ['B', 'R', 'G'],
 ['B', 'B', 'G', 'Y']
]

case_hard = [
 ['B'], 
 ['Y', 'Y', 'Y', 'Y'], 
 ['G', 'G', 'G', 'G'], 
 ['R','R','R', 'R'], 
 ['B','B', 'B']
]

Ecco il codice A *:

from copy import deepcopy
from heapq import *
import time, sys
import textdistance
import os

def a_star(b, goal, h):
    print("A*")
    start_time = time.time()
    heap = [(-1, b)]
    bib = {}
    bib[b.stringify()] = b

    while len(heap) > 0:
        node = heappop(heap)[1]
        if node == goal:
            print("Number of explored states: {}".format(len(bib)))
            elapsed_time = time.time() - start_time
            print("Execution time {}".format(elapsed_time))
            return rebuild_path(node)

        valid_moves = node.get_valid_moves()
        children = node.get_children(valid_moves)
        for m in children:
          key = m.stringify()
          if key not in bib.keys():
            h_n = h(key, goal.stringify())
            heappush(heap, (m.g + h_n, m)) 
            bib[key] = m

    elapsed_time = time.time() - start_time
    print("Execution time {}".format(elapsed_time))
    print('No Solution')

Ecco il codice IDA *:

#shows the moves done to solve the puzzle
def rebuild_path(state):
    path = []
    while state.parent != None:
        path.insert(0, state)
        state = state.parent
    path.insert(0, state)
    print("Number of steps to solve: {}".format(len(path) - 1))
    print('Solution')

def ida_star(root, goal, h):
    print("IDA*")
    start_time = time.time()
    bound = h(root.stringify(), goal.stringify())
    path = [root]
    solved = False
    while not solved:
        t = search(path, 0, bound, goal, h)
        if type(t) == Board:
            solved = True
            elapsed_time = time.time() - start_time
            print("Execution time {}".format(elapsed_time))
            rebuild_path(t)
            return t
        bound = t

def search(path, g, bound, goal, h):

    node = path[-1]
    time.sleep(0.005)
    f = g + h(node.stringify(), goal.stringify())

    if f > bound: return f
    if node == goal:
        return node

    min_cost = float('inf')
    heap = []
    valid_moves = node.get_valid_moves()
    children = node.get_children(valid_moves)
    for m in children:
      if m not in path:
        heappush(heap, (m.g + h(m.stringify(), goal.stringify()), m)) 

    while len(heap) > 0:
        path.append(heappop(heap)[1])
        t = search(path, g + 1, bound, goal, h)
        if type(t) == Board: return t
        elif t < min_cost: min_cost = t
        path.pop()
    return min_cost

class Board:
  def __init__(self, board, parent=None, g=0, last_moved_piece=''):
    self.board = board
    self.capacity = len(board[0])
    self.g = g
    self.parent = parent
    self.piece = last_moved_piece

  def __lt__(self, b):
    return self.g < b.g

  def __call__(self):
    return self.stringify()

  def __eq__(self, b):
    if self is None or b is None: return False
    return self.stringify() == b.stringify()

  def __repr__(self):
    return '\n'.join([' '.join([j[0] for j in i]) for i in self.board])+'\n\n'

  def stringify(self):
    b=''
    for i in self.board:
      a = ''.join([j[0] for j in i])
      b += a + '-' * (self.capacity-len(a))

    return b

  def get_valid_moves(self):
    pos = []
    for i in range(len(self.board)):
      if len(self.board[i]) < self.capacity:
        pos.append(i)
    return pos

  def get_children(self, moves):
    children = []
    for i in range(len(self.board)):
      for j in moves:
        if i != j and self.board[i][-1] != self.piece:
          a = deepcopy(self.board)
          piece = a[i].pop()
          a[j].append(piece)
          children.append(Board(a, self, self.g+1, piece))
    return children

Uso:

initial = Board(start)
final1 = Board(case_easy)
final2 = Board(case_medium)
final2a = Board(case_medium2)
final3 = Board(case_hard)

x = textdistance.gotoh.distance

a_star(initial, final1, x)
a_star(initial, final2, x)
a_star(initial, final2a, x)

ida_star(initial, final1, x)
ida_star(initial, final2, x)
ida_star(initial, final2a, x)

0

Nei commenti hai detto che ci sono N pile con capacità P e ci sono sempre P spazi vuoti. In tal caso, sembra che questo algoritmo funzionerà nella elseclausola del codice (ovvero quando num_removals + min_to_unlock > num_free_spaces):

  1. Trova il pezzo desiderato più vicino alla cima di una pila.
  2. Sposta tutti i pezzi sopra il pezzo desiderato in modo tale che ci sia una pila (non la pila di destinazione) che ha uno spazio vuoto in cima. Se necessario, sposta i pezzi dalla pila di destinazione o da un'altra pila. Se l'unico spazio aperto è la parte superiore della pila di destinazione, sposta un pezzo lì per aprire la parte superiore di un'altra pila. Questo è sempre possibile, perché ci sono spazi aperti P e al massimo pezzi P-1 per spostarsi da sopra il pezzo desiderato.
  3. Sposta il pezzo desiderato nel punto vuoto sopra una pila.
  4. Sposta i pezzi dalla pila di destinazione fino a quando la destinazione è aperta.
  5. Sposta il pezzo desiderato nella destinazione.

Ho passato le ultime due ore a scavare in questa risposta e penso che ci potrebbe essere qualcosa lì. Se possibile, potresti fornire un po 'più di informazioni su come faresti a spostare i pezzi che si trovano sopra il pezzo desiderato? Come si determinano in quali stack spostarli? Forse un po 'di psuedocode / code. Questo è sicuramente il più vicino che ho sentito per risolvere questo finora.
Tristen,

0

Anche se non ho trovato il tempo per dimostrarlo matematicamente, ho deciso di pubblicarlo comunque; spero che sia d'aiuto. L'approccio consiste nel definire un parametro p che diminuisce con buone mosse e raggiunge lo zero esattamente al termine del gioco. Nel programma considera solo le mosse buone o neutre (che lasciano invariato p) e dimentica le mosse sbagliate (che aumentano p).

Quindi cos'è p? Per ogni colonna, definire p come il numero di blocchi che devono ancora essere rimossi prima che tutti i colori in quella colonna siano il colore desiderato. Supponiamo quindi che vogliamo che i blocchi rossi finiscano nella colonna più a sinistra (tornerò più avanti), e supponiamo che ci sia un blocco rosso in fondo, quindi un giallo sopra, un altro blocco sopra quello e poi uno spazio vuoto. Quindi p = 2 per questa colonna (due blocchi da rimuovere prima che tutti siano rossi). Calcola p per tutte le colonne. Per la colonna che dovrebbe finire vuota, p è uguale al numero di blocchi che contiene (dovrebbero andare tutti). P per lo stato corrente è la somma di tutte le p per tutte le colonne.

Quando p = 0, tutte le colonne hanno lo stesso colore e una colonna è vuota, quindi il gioco è finito.

Scegliendo mosse che riducono p (o almeno non aumentano p) ci stiamo muovendo nella giusta direzione, questa è secondo me la differenza cruciale con gli algoritmi del percorso più breve: Dijkstra non aveva idea se si stesse muovendo nella giusta direzione con ogni vertice su cui stava indagando.

Quindi, come possiamo determinare dove dovrebbe finire ogni colore? Fondamentalmente determinando p per ogni possibilità. Quindi ad es. Iniziare con rosso / giallo / verde / vuoto, calcolare p, quindi andare a rosso / giallo / vuoto / verde, calcolare p, ecc. Prendere la posizione iniziale con il p più basso. Questo richiede n! calcoli. Per n = 8 questo è 40320, il che è fattibile. La cattiva notizia è che dovrai esaminare tutte le posizioni di partenza con lo stesso p più basso. La buona notizia è che puoi dimenticare il resto.

Ci sono due incertezze matematiche qui. Uno: è possibile che esista un percorso più breve che utilizza una mossa sbagliata? Sembra improbabile, non ho trovato un controesempio, ma non ho nemmeno trovato una prova. Due: è possibile che quando si inizia con una posizione di partenza non ottimale (cioè non la p più bassa) ci sia un percorso più breve rispetto a tutte le posizioni di partenza ottimali. Ancora: nessun controesempio ma nessuna prova.

Alcuni suggerimenti di implementazione. Tenere traccia di p durante l'esecuzione per ogni colonna non è difficile, ma ovviamente dovrebbe essere fatto. Un altro parametro che dovrebbe essere mantenuto per ogni colonna è il numero di punti aperti. Se 0, questa colonna non può momentaneamente accettare alcun blocco, quindi può essere lasciata fuori dal ciclo. Quando p = 0 per una colonna, non è idoneo per un pop. Per ogni possibile pop, esamina se c'è una buona mossa, vale a dire una che diminuisce il totale p. Se ce ne sono più, esamina tutto. Se non ce n'è, considera tutte le mosse neutrali.

Tutto ciò dovrebbe ridurre notevolmente i tempi di calcolo.


1
Penso che tu abbia frainteso la domanda! Sebbene questa sia la motivazione alla base della domanda. La domanda è trovare il numero minimo di mosse per spostare un singolo pezzo, in una singola posizione. La domanda non era quella di trovare il numero minimo di mosse per ordinare le pile, sebbene questa sia la motivazione alla base della domanda. Tuttavia, con quel punteggio di P, non saresti corretto. Ci sono molti casi in cui ci sono "mosse sbagliate" che finiscono inizialmente con l'aumento di P, per poi ridurlo a una velocità maggiore. Detto questo, forse rileggi la domanda poiché la tua risposta non ha rilevanza.
Tristen,

1
Mi scuso Tristen, in effetti non ho letto attentamente la domanda. Ero affascinato dall'aspetto matematico e, essendo in ritardo alla festa, troppo veloce per rispondere. La prossima volta starò più attento. Spero che tu trovi una risposta.
Paul Rene,
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.