C99 - scheda 3x3 in 0,084s
Modifica: ho riformattato il mio codice e fatto un'analisi più approfondita dei risultati.
Ulteriori modifiche: aggiunta potatura per simmetrie. Questo rende 4 configurazioni dell'algoritmo: con o senza simmetrie X con o senza potatura alfa-beta
Modifiche più lontane: aggiunta la memoizzazione usando una tabella hash, ottenendo finalmente l'impossibile: risolvere una scheda 3x3!
Caratteristiche primarie:
- implementazione diretta di minimax con potatura alfa-beta
- pochissima gestione della memoria (mantiene la dll di mosse valide; O (1) aggiornamenti per ramo nella ricerca dell'albero)
- secondo file con potatura per simmetrie. Ottiene ancora aggiornamenti O (1) per ramo (tecnicamente O (S) dove S è il numero di simmetrie. Questo è 7 per le schede quadrate e 3 per le schede non quadrate)
- il terzo e il quarto file aggiungono memoization. Hai il controllo sulla dimensione dell'hashtable (
#define HASHTABLE_BITWIDTH
). Quando questa dimensione è maggiore o uguale al numero di pareti, non garantisce collisioni e aggiornamenti O (1). Gli hashtable più piccoli avranno più collisioni e saranno leggermente più lenti.
- compilare con
-DDEBUG
per le stampe
Potenziali miglioramenti:
risolto il problema con la perdita di memoria ridotta nella prima modifica
potatura alfa / beta aggiunta nella seconda modifica
simmetrie di potatura aggiunte nella terza modifica (si noti che le simmetrie non sono gestite dalla memoizzazione, quindi rimane un'ottimizzazione separata.)
memoization aggiunta nella 4a modifica
- attualmente la memoization utilizza un bit indicatore per ciascun muro. Una scheda 3x4 ha 31 pareti, quindi questo metodo non è in grado di gestire schede 4x4 indipendentemente dai vincoli temporali. il miglioramento sarebbe emulare gli interi X-bit, dove X è almeno pari al numero di muri.
Codice
A causa della mancanza di organizzazione, il numero di file è cresciuto fuori controllo. Tutto il codice è stato spostato in questo repository Github . Nella modifica della memoization, ho aggiunto un makefile e uno script di test.
risultati
Note sulla complessità
Gli approcci a forza bruta a punti e scatole esplodono in complessità molto rapidamente .
Considera una tavola con R
righe e C
colonne. Ci sono R*C
quadrati, R*(C+1)
pareti verticali e C*(R+1)
pareti orizzontali. Questo è un totale di W = 2*R*C + R + C
.
Poiché Lembik ci ha chiesto di risolvere il gioco con minimax, dobbiamo attraversare le foglie dell'albero del gioco. Ignoriamo la potatura per ora, perché ciò che conta sono gli ordini di grandezza.
Ci sono W
opzioni per la prima mossa. Per ognuno di questi, il giocatore successivo può giocare a qualsiasi delle W-1
pareti rimanenti, ecc. Questo ci dà uno spazio di ricerca di SS = W * (W-1) * (W-2) * ... * 1
, o SS = W!
. I fattoriali sono enormi, ma è solo l'inizio. SS
è il numero di nodi foglia nello spazio di ricerca. Più rilevante per la nostra analisi è il numero totale di decisioni che dovevano essere prese (cioè il numero di rami B
nella struttura). Il primo strato di rami ha W
opzioni. Per ognuno di questi, il livello successivo ha W-1
, ecc.
B = W + W*(W-1) + W*(W-1)*(W-2) + ... + W!
B = SUM W!/(W-k)!
k=0..W-1
Diamo un'occhiata ad alcune dimensioni di piccoli tavoli:
Board Size Walls Leaves (SS) Branches (B)
---------------------------------------------------
1x1 04 24 64
1x2 07 5040 13699
2x2 12 479001600 1302061344
2x3 17 355687428096000 966858672404689
Questi numeri stanno diventando ridicoli. Almeno spiegano perché il codice della forza bruta sembra rimanere per sempre su una scheda 2x3. Lo spazio di ricerca di una scheda 2x3 è 742560 volte più grande di 2x2 . Se il completamento di 2x2 richiede 20 secondi, un'estrapolazione conservativa prevede oltre 100 giorni di tempo di esecuzione per 2x3. Chiaramente dobbiamo potare.
Analisi di potatura
Ho iniziato aggiungendo una potatura molto semplice usando l'algoritmo alpha-beta. Fondamentalmente, smette di cercare se un avversario ideale non gli darebbe mai le sue attuali opportunità. "Ehi guarda, vinco di molto se il mio avversario mi permette di ottenere tutti i quadrati!", Mai pensato AI.
modifica Ho anche aggiunto la potatura basata su pannelli simmetrici. Non uso un approccio di memoization, nel caso in cui un giorno aggiungessi memoization e voglio mantenere quell'analisi separata. Invece, funziona così: la maggior parte delle linee ha una "coppia simmetrica" da qualche altra parte sulla griglia. Esistono fino a 7 simmetrie (orizzontale, verticale, 180 rotazione, 90 rotazione, 270 rotazione, diagonale e l'altra diagonale). Tutti e 7 si applicano alle schede quadrate, ma le ultime 4 non si applicano alle schede non quadrate. Ogni muro ha un puntatore alla sua "coppia" per ognuna di queste simmetrie. Se, andando in un turno, la scacchiera è simmetrica orizzontalmente, allora solo una di ciascuna coppia orizzontale deve essere giocata.
modifica modifica Memoization! Ogni muro ottiene un ID univoco, che ho opportunamente impostato per essere un bit indicatore; l'ennesimo muro ha l'id 1 << n
. L'hash di una tavola, quindi, è solo l'OR di tutte le pareti giocate. Questo viene aggiornato ad ogni filiale in O (1) volta. La dimensione dell'hashtable è impostata in a #define
. Tutti i test sono stati eseguiti con dimensioni 2 ^ 12, perché perché no? Quando ci sono più muri che bit che indicizzano la tabella hash (12 bit in questo caso), i 12 meno significativi vengono mascherati e usati come indice. Le collisioni vengono gestite con un elenco collegato in ciascun indice hashtable. Il seguente grafico è la mia analisi rapida e dettagliata di come le dimensioni hashtable influiscono sulle prestazioni. Su un computer con RAM infinita, impostiamo sempre le dimensioni della tabella sul numero di muri. Una scheda 3x4 avrebbe una lunghezza hash di 2 ^ 31. Purtroppo non abbiamo quel lusso.
Ok, torniamo alla potatura .. Fermando la ricerca in alto nell'albero, possiamo risparmiare molto tempo non scendendo alle foglie. Il "fattore di potatura" è la frazione di tutti i rami possibili che abbiamo dovuto visitare. La forza bruta ha un fattore di potatura di 1. Più è piccola, meglio è.