Tenere traccia degli stati visitati nella ricerca breadth-first


10

Quindi stavo cercando di implementare BFS in un puzzle di Sliding Blocks (tipo di numero). Ora la cosa principale che ho notato è che se hai una 4*4scheda il numero di stati può essere tanto grande quanto 16!non posso elencare in anticipo tutti gli stati.

Quindi la mia domanda è: come posso tenere traccia degli stati già visitati? (Sto usando una scheda di classe ogni istanza di classe contiene un modello di scheda univoco e viene creata elencando tutti i possibili passaggi del passaggio corrente).

Ho cercato in rete e apparentemente non tornano al passaggio precedente appena completato, MA possiamo tornare al passaggio precedente anche da un altro percorso e quindi ri-enumerare nuovamente tutti i passaggi che sono stati precedentemente visitati. Quindi, come tenere traccia degli stati visitati quando tutti gli stati non sono già stati elencati? (confrontare gli stati già presenti con il passaggio attuale sarà costoso).


1
Nota a margine: non riuscivo a pensare a uno stack più appropriato per pubblicare questa domanda. Sono consapevole che i dettagli di implementazione non sono generalmente benvenuti in questo Stack.
DuttaA

2
imo questa è una grande domanda per SE: AI perché non si tratta solo di implementazione, ma del concetto stesso. Per non parlare, la domanda ha attratto 4 risposte legittime nel giro di poche ore. (Ha preso la libertà di modificare il titolo per la ricerca e creare un tag BFS)
DukeZhou

Risposte:


8

Puoi usare un set(nel senso matematico della parola, cioè una collezione che non può contenere duplicati) per memorizzare gli stati che hai già visto. Le operazioni che devi essere in grado di eseguire su questo sono:

  • inserimento di elementi
  • verifica se gli elementi sono già presenti

Praticamente ogni linguaggio di programmazione dovrebbe già avere il supporto per una struttura di dati che può eseguire entrambe queste operazioni in tempo costante ( ). Per esempio:O(1)

  • set in Python
  • HashSet in Java

A prima vista, può sembrare che aggiungere tutti gli stati che vedi in un set come questo sia costoso dal punto di vista della memoria, ma non è poi così male in confronto alla memoria di cui hai già bisogno per la tua frontiera; se il tuo fattore di ramificazione è , la tua frontiera crescerà di b - 1 elementi per nodo che visiti (rimuovi 1 nodo dalla frontiera per "visitarlo", aggiungi b nuovi successori / figli), mentre il tuo set crescerà solo di 1 extra nodo per nodo visitato.BB-11B1

In pseudocodice, un tale set (chiamiamolo closed_set, per essere coerenti con lo pseudocodice su wikipedia potrebbe essere usato in una ricerca breadth-first come segue:

frontier = First-In-First-Out Queue
frontier.add(initial_state)

closed_set = set()

while frontier not empty:
    current = frontier.remove_next()

    if current == goal_state:
        return something

    for each child in current.generate_children()
        if child not in closed_set:    // This operation should be supported in O(1) time regardless of closed_set's current size
            frontier.add(child)

    closed_set.add(current)    // this should also run in O(1) time

(alcune varianti di questo pseudocodice potrebbero anche funzionare, e potrebbero essere più o meno efficienti a seconda della situazione; ad esempio, potresti anche prendere closed_setper contenere tutti i nodi di cui hai già aggiunto dei bambini alla frontiera, quindi evitare completamente la generate_children()chiamata se currentè già nel closed_set.)


Quello che ho descritto sopra sarebbe il modo standard di gestire questo problema. Intuitivamente, sospetto che una "soluzione" diversa potrebbe essere quella di randomizzare sempre l'ordine di un nuovo elenco di stati successori prima di aggiungerli alla frontiera. In questo modo, non eviti il ​​problema di aggiungere occasionalmente stati che hai già precedentemente espanso alla frontiera, ma penso che dovrebbe ridurre significativamente il rischio di rimanere bloccati in cicli infiniti.

Fai attenzione : non conosco alcuna analisi formale di questa soluzione che dimostri che evita sempre cicli infiniti. Se provo a "farcela" in modo intuitivo, sospetto che dovrebbe funzionare, e non richiede memoria aggiuntiva. Potrebbero esserci casi limite a cui non sto pensando in questo momento, quindi semplicemente potrebbe non funzionare, la soluzione standard sopra descritta sarà una scommessa più sicura (al costo di più memoria).


1
Potrò

3
SO(1)S

1
@DuttaA Ho aggiunto alcuni pseudocodici per descrivere esattamente come verrà utilizzato il set, si spera che ciò possa chiarire qualcosa. Si noti che non abbiamo mai mai attraversato l'intero closed_set, le sue dimensioni non dovrebbero mai influenzare il nostro tempo di calcolo (asintotico).
Dennis Soemers,

1
In realtà lo stavo facendo usando c ++ Non ho idea di hashing ... Immagino che userò Python ora ... Grazie per la risposta
DuttaA

3
@DuttaA In C ++ probabilmente vorresti usare uno std ::
unordered_set

16

La risposta di Dennis Soemers è corretta: dovresti usare un HashSet o una struttura simile per tenere traccia degli stati visitati in Ricerca grafico BFS.

Tuttavia, non risponde esattamente alla tua domanda. Hai ragione, nel peggiore dei casi, BFS richiederà quindi di memorizzarne 16! nodi. Anche se i tempi di inserimento e controllo nel set saranno O (1), avrai comunque bisogno di una quantità assurda di memoria.

Per risolvere questo problema, non utilizzare BFS . È intrattabile per tutti tranne che per i problemi più semplici, perché richiede sia il tempo che la memoria che sono esponenziali in lontananza rispetto allo stato obiettivo più vicino.

Un algoritmo molto più efficiente in termini di memoria è l' approfondimento iterativo . Ha tutte le proprietà desiderabili di BFS, ma usa solo la memoria O (n), dove n è il numero di mosse per raggiungere la soluzione più vicina. Potrebbe volerci ancora un po ', ma colpirai i limiti di memoria molto prima dei limiti relativi alla CPU.

Meglio ancora, sviluppa un'euristica specifica del dominio e usa la ricerca A * . Ciò dovrebbe richiedere di esaminare solo un numero molto limitato di nodi e consentire alla ricerca di completarsi in qualcosa di molto più vicino al tempo lineare.


2
16!

@DennisSoemers ha ragione..hai ragione anche tu .. stavo solo cercando di affinare le mie abilità ...
Passerò a

ci sono casi in cui BFS può restituire soluzioni locali accettabili? (Ho a che fare con altri come 81! * Tbd-value e breadth-first sembrano ottimali in quanto ci sono fattori bloccanti che possono essere facili da perdere senza stanchezza. Al momento non siamo preoccupati per un gioco veramente forte, piuttosto "rispettabilmente "prestazioni generali deboli su una serie di topologie di gioco imprevedibili.)
DukeZhou

2
@DukeZhou BFS viene solitamente utilizzato solo quando si cerca una soluzione completa. Per interromperlo in anticipo, è necessaria una funzione che stima la qualità relativa di diverse soluzioni parziali, ma se si dispone di una tale funzione, probabilmente è possibile utilizzare A * invece!
John Doucette

Invece di dire "il numero di mosse nel gioco", consiglierei "il numero minimo di mosse per raggiungere l'obiettivo". Penserei al numero di mosse nel gioco come a ogni mossa che ti porta da uno dei 16! afferma a qualsiasi altro, che è molta più memoria di quanto usi approfondimenti iterativi.
NotThatGuy

7

Mentre le risposte fornite sono generalmente vere, un BFS nel 15 puzzle non è solo abbastanza fattibile, ma è stato fatto nel 2005! Il documento che descrive l'approccio può essere trovato qui:

http://www.aaai.org/Papers/AAAI/2005/AAAI05-219.pdf

Alcuni punti chiave:

  • Per fare ciò, era necessaria la memoria esterna, ovvero BFS utilizzava il disco rigido per l'archiviazione anziché la RAM.
  • In realtà ci sono solo 15! / 2 stati, poiché lo spazio degli stati ha due componenti reciprocamente irraggiungibili.
  • Questo funziona nel puzzle a tessere scorrevoli perché gli spazi degli stati crescono molto lentamente da un livello all'altro. Ciò significa che la memoria totale richiesta per qualsiasi livello è molto più piccola dell'intera dimensione dello spazio degli stati. (Questo contrasta con uno spazio degli stati come il cubo di Rubik, dove lo spazio degli stati cresce molto più rapidamente.)
  • Poiché il puzzle a tessere scorrevoli non è diretto, devi solo preoccuparti dei duplicati nel livello corrente o precedente. In uno spazio diretto è possibile generare duplicati in qualsiasi livello precedente della ricerca, il che rende le cose molto più complicate.
  • Nel lavoro originale di Korf (linkato sopra) in realtà non memorizzavano il risultato della ricerca - la ricerca calcolava semplicemente quanti stati c'erano per ogni livello. Se si desidera memorizzare i primi risultati, è necessario qualcosa come WMBFS ( http://www.cs.du.edu/~sturtevant/papers/bfs_min_write.pdf )
  • Esistono tre approcci primari per confrontare gli stati dai livelli precedenti quando gli stati sono memorizzati su disco.
    • Il primo è basato sull'ordinamento. Se si ordinano due file di successori, è possibile scansionarli in ordine lineare per trovare duplicati.
    • Il secondo è basato sull'hash. Se si utilizza una funzione hash per raggruppare i successori in file, è possibile caricare file più piccoli dello spazio dello stato completo per verificare la presenza di duplicati. (Notare che ci sono due funzioni hash qui: una per inviare uno stato a un file e una per differenziare gli stati all'interno di quel file.)
    • Il terzo è il rilevamento di duplicati strutturati. Questa è una forma di rilevamento basato sull'hash, ma viene eseguita in modo tale che i duplicati possano essere controllati immediatamente quando vengono generati anziché dopo che sono stati generati tutti.

C'è molto altro da dire qui, ma i documenti sopra forniscono molti più dettagli.


È un'ottima risposta..ma non per i rumori come me:) ... Non sono un esperto di programmatore ..
DuttaA

In che modo il non indiretto ti aiuterebbe a evitare i duplicati in altri livelli? Sicuramente saresti in grado di tornare a un nodo in un altro livello spostando 3 tessere in un cerchio. Semmai, diretto ti aiuterebbe a evitare i duplicati, perché è più restrittivo. Il documento collegato parla del rilevamento di duplicati, ma non menziona affatto il reindirizzamento diretto o indiretto, né sembra menzionare l'evitamento di duplicati a diversi livelli (ma potrei averlo perso nella mia brevissima scansione).
NotThatGuy

@NotThatGuy In un grafico non orientato un genitore e un figlio si trovano a una distanza maggiore di 1 nella profondità che si trovano nel BFS. Questo perché una volta trovato l'uno, il bordo non diretto garantisce che l'altro verrà trovato immediatamente dopo. Ma, in un grafico diretto, uno stato in profondità 10 può generare figli in profondità 2, perché il bambino in profondità 2 non deve avere un vantaggio sull'altro stato (ciò renderebbe profondità 3 anziché profondità 10) .
Nathan S.

@NotThatGuy Se sposti 3 tessere in un cerchio crei un ciclo, ma un BFS lo esplorerà simultaneamente in entrambe le direzioni, quindi non ti riporterà a profondità molto più basse. In questa demo viene mostrata l'intera tessera scorrevole 3x2, e puoi seguire i cicli per vedere come si verificano: movingai.com/SAS/IDA
Nathan S.

1
grandiosità. Benvenuti in SE: AI!
DukeZhou

3

Ironia della sorte, la risposta è "usa qualunque sistema tu voglia". Un hashSet è una buona idea. Tuttavia, risulta che le tue preoccupazioni sull'uso della memoria sono infondate. BFS è così grave con questo tipo di problemi, che risolve questo problema per te.

Considera che il tuo BFS richiede di mantenere una pila di stati non elaborati. Man mano che avanzi nel puzzle, gli stati con cui hai a che fare diventano sempre più diversi, quindi è probabile che ogni strato del tuo BFS moltiplichi il numero di stati da esaminare per circa 3.

Ciò significa che, quando stai elaborando l'ultimo strato del tuo BFS, devi avere almeno 16 stati / 3 in memoria. Qualunque approccio tu abbia utilizzato per assicurarti che l'adattamento in memoria sia sufficiente a garantire che anche l'elenco precedentemente visitato si adatti alla memoria.

Come altri hanno sottolineato, questo non è l'algoritmo migliore da usare. Utilizzare un algoritmo che si adatta meglio al problema.


2

Il problema dei 15 puzzle si gioca su una tavola 4x4. L'implementazione di questo nel codice sorgente viene eseguita in modo graduale. Inizialmente, il motore di gioco stesso deve essere programmato. Ciò consente di giocare da un operatore umano. Il gioco a 15 puzzle ha un solo elemento libero e su questo elemento le azioni vengono eseguite. Il motore di gioco accetta quattro possibili comandi: sinistra, destra, su e giù. Altre azioni non sono consentite ed è possibile controllare il gioco solo con queste istruzioni.

Il livello successivo per giocare è una GUI. Questo è molto importante, perché consente di testare il motore di gioco e provare a risolvere il gioco a mano. Inoltre, una GUI è importante perché dobbiamo capire la potenziale euristica. E ora possiamo parlare dell'IA stessa. L'intelligenza artificiale deve inviare comandi al motore di gioco (sinistra, destra, su e giù). Un approccio ingenuo per un risolutore sarebbe un algoritmo di ricerca della forza bruta, il che significa che l'IA sta inviando comandi casuali fino al raggiungimento dello stato obiettivo. Un'idea più avanzata è quella di implementare una sorta di database di pattern che riduca lo spazio degli stati. L'ampiezza della prima ricerca non è direttamente un'euristica, ma è un inizio. È uguale a creare un grafico per testare possibili movimenti in modo cronologico.

Il monitoraggio degli stati esistenti può essere eseguito con un grafico. Ogni stato è un nodo, ha un ID e un ID principale. L'intelligenza artificiale può aggiungere ed eliminare nodi nel grafico e il pianificatore può risolvere il grafico per trovare un percorso verso l'obiettivo. Dal punto di vista della programmazione, un motore di gioco del 15 puzzle è l'oggetto e l'elenco di molti oggetti è un arraylist. Sono memorizzati in una classe di grafici. Comprenderlo nel codice sorgente è un po 'complicato, di solito la prima prova fallirà e il progetto produrrà molti errori. Per gestire la complessità, un tale progetto viene solitamente svolto in un progetto accademico, ciò significa che è un argomento per scrivere un articolo su di esso che può avere 100 pagine e più.


1

Approcci al gioco

16!

Tuttavia, questi fatti banali non sono pertinenti se l'obiettivo è completare il puzzle nel minor numero di cicli di calcolo. L'ampiezza della prima ricerca non è un modo pratico per completare un puzzle di mossa ortogonale. Il costo molto elevato di una prima ricerca ampia sarebbe necessario solo se il numero di mosse è di fondamentale importanza per qualche motivo.

Discesa sotto-sequenza

La maggior parte dei vertici che rappresentano gli stati non sarà mai visitata e ogni stato visitato può avere tra due e quattro bordi in uscita. Ogni blocco ha una posizione iniziale e una posizione finale e la scheda è simmetrica. La massima libertà di scelta esiste quando lo spazio aperto è una delle quattro posizioni centrali. Il minimo è quando lo spazio aperto è una delle quattro posizioni d'angolo.

Una ragionevole funzione di disparità (errore) è semplicemente la somma di tutte le disparità x più la somma di tutte le disparità y e un numero che rappresenta euristicamente quale dei tre livelli di libertà di movimento esiste a causa del posizionamento risultante dello spazio aperto (medio, bordo , angolo).

Sebbene i blocchi possano temporaneamente allontanarsi dalle loro destinazioni per supportare una strategia verso il completamento che richiede una sequenza di mosse, raramente si verifica un caso in cui tale strategia superi otto mosse, generando, in media, 5.184 permutazioni per le quali è possibile confrontare gli stati finali utilizzando la funzione di disparità sopra.

Se lo spazio vuoto e le posizioni dei blocchi da 1 a 15 sono codificati come una matrice di stuzzichini, sono necessarie solo le operazioni di addizione, sottrazione e bit per rendere veloce l'algoritmo. Ripetendo le otto mosse, le strategie di forza bruta possono essere ripetute fino a quando la disparità scende a zero.

Sommario

Questo algoritmo non può scorrere perché esiste sempre almeno una delle permutazioni di otto mosse che diminuisce la disparità, indipendentemente dallo stato iniziale, ad eccezione di uno stato iniziale che è già completo.

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.