C, con una media di 500+ 1500 1750 punti
Questo è un miglioramento relativamente minore rispetto alla versione 2 (vedi sotto per le note sulle versioni precedenti). Ci sono due parti. Primo: invece di selezionare casualmente le schede dal pool, il programma ora scorre su tutte le board del pool, usandole a turno prima di tornare in cima al pool e ripetere. (Poiché il pool viene modificato mentre si verifica questa iterazione, ci saranno comunque board che vengono scelte due volte di seguito, o peggio, ma questo non è un problema serio.) La seconda modifica è che il programma ora tiene traccia quando il pool cambia e se il programma dura troppo a lungo senza migliorare il contenuto del pool, determina che la ricerca si è "bloccata", svuota il pool e ricomincia da capo con una nuova ricerca. Continua a farlo fino a quando non sono trascorsi i due minuti.
Inizialmente avevo pensato che avrei impiegato una sorta di ricerca euristica per andare oltre il raggio di 1500 punti. Il commento di @ mellamokb su una tavola da 4527 punti mi ha portato a ritenere che ci fosse un ampio margine di miglioramento. Tuttavia, stiamo usando un elenco di parole relativamente piccolo. La tavola da 4527 punti segnava usando YAWL, che è la lista di parole più inclusiva là fuori - è persino più grande della lista di parole ufficiale degli Scrabble degli Stati Uniti. Con questo in mente, ho riesaminato le schede trovate dal mio programma e ho notato che sembrava esserci un numero limitato di schede oltre il 1700 circa. Quindi, per esempio, ho avuto più tiri che avevano scoperto una tavola segnando 1726, ma era sempre la stessa identica tavola che veniva trovata (ignorando rotazioni e riflessioni).
Come altro test, ho eseguito il mio programma usando YAWL come dizionario e ho trovato la scheda a 4527 punti dopo circa una dozzina di esecuzioni. Detto questo, sto ipotizzando che il mio programma sia già al limite superiore dello spazio di ricerca, e quindi la riscrittura che stavo pianificando introdurrebbe ulteriore complessità per un guadagno molto piccolo.
Ecco la mia lista delle cinque schede con il punteggio più alto che il mio programma ha trovato usando la lista di english.0
parole:
1735 : D C L P E I A E R N T R S E G S
1738 : B E L S R A D G T I N E S E R S
1747 : D C L P E I A E N T R D G S E R
1766 : M P L S S A I E N T R N D E S G
1772: G R E P T N A L E S I T D R E S
La mia convinzione è che la "scheda grep" del 1772 (come ho preso per chiamarla), con 531 parole, è la scheda con il punteggio più alto possibile con questo elenco di parole. Oltre il 50% della durata di due minuti del mio programma termina con questa scheda. Ho anche lasciato il mio programma in esecuzione durante la notte senza trovare nulla di meglio. Quindi, se esiste una tavola con punteggi più alti, probabilmente dovrebbe avere qualche aspetto che sconfigge la tecnica di ricerca del programma. Una scheda in cui ogni possibile piccola modifica al layout provoca un enorme calo del punteggio totale, ad esempio, potrebbe non essere mai scoperto dal mio programma. La mia impressione è che è molto improbabile che esista una tavola del genere.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#define WORDLISTFILE "./english.0"
#define XSIZE 4
#define YSIZE 4
#define BOARDSIZE (XSIZE * YSIZE)
#define DIEFACES 6
#define WORDBUFSIZE 256
#define MAXPOOLSIZE 32
#define STALLPOINT 64
#define RUNTIME 120
/* Generate a random int from 0 to N-1.
*/
#define random(N) ((int)(((double)(N) * rand()) / (RAND_MAX + 1.0)))
static char const dice[BOARDSIZE][DIEFACES] = {
"aaeegn", "elrtty", "aoottw", "abbjoo",
"ehrtvw", "cimotu", "distty", "eiosst",
"delrvy", "achops", "himnqu", "eeinsu",
"eeghnw", "affkps", "hlnnrz", "deilrx"
};
/* The dictionary is represented in memory as a tree. The tree is
* represented by its arcs; the nodes are implicit. All of the arcs
* emanating from a single node are stored as a linked list in
* alphabetical order.
*/
typedef struct {
int letter:8; /* the letter this arc is labelled with */
int arc:24; /* the node this arc points to (i.e. its first arc) */
int next:24; /* the next sibling arc emanating from this node */
int final:1; /* true if this arc is the end of a valid word */
} treearc;
/* Each of the slots that make up the playing board is represented
* by the die it contains.
*/
typedef struct {
unsigned char die; /* which die is in this slot */
unsigned char face; /* which face of the die is showing */
} slot;
/* The following information defines a game.
*/
typedef struct {
slot board[BOARDSIZE]; /* the contents of the board */
int score; /* how many points the board is worth */
} game;
/* The wordlist is stored as a binary search tree.
*/
typedef struct {
int item: 24; /* the identifier of a word in the list */
int left: 16; /* the branch with smaller identifiers */
int right: 16; /* the branch with larger identifiers */
} listnode;
/* The dictionary.
*/
static treearc *dictionary;
static int heapalloc;
static int heapsize;
/* Every slot's immediate neighbors.
*/
static int neighbors[BOARDSIZE][9];
/* The wordlist, used while scoring a board.
*/
static listnode *wordlist;
static int listalloc;
static int listsize;
static int xcursor;
/* The game that is currently being examined.
*/
static game G;
/* The highest-scoring game seen so far.
*/
static game bestgame;
/* Variables to time the program and display stats.
*/
static time_t start;
static int boardcount;
static int allscores;
/* The pool contains the N highest-scoring games seen so far.
*/
static game pool[MAXPOOLSIZE];
static int poolsize;
static int cutoffscore;
static int stallcounter;
/* Some buffers shared by recursive functions.
*/
static char wordbuf[WORDBUFSIZE];
static char gridbuf[BOARDSIZE];
/*
* The dictionary is stored as a tree. It is created during
* initialization and remains unmodified afterwards. When moving
* through the tree, the program tracks the arc that points to the
* current node. (The first arc in the heap is a dummy that points to
* the root node, which otherwise would have no arc.)
*/
static void initdictionary(void)
{
heapalloc = 256;
dictionary = malloc(256 * sizeof *dictionary);
heapsize = 1;
dictionary->arc = 0;
dictionary->letter = 0;
dictionary->next = 0;
dictionary->final = 0;
}
static int addarc(int arc, char ch)
{
int prev, a;
prev = arc;
a = dictionary[arc].arc;
for (;;) {
if (dictionary[a].letter == ch)
return a;
if (!dictionary[a].letter || dictionary[a].letter > ch)
break;
prev = a;
a = dictionary[a].next;
}
if (heapsize >= heapalloc) {
heapalloc *= 2;
dictionary = realloc(dictionary, heapalloc * sizeof *dictionary);
}
a = heapsize++;
dictionary[a].letter = ch;
dictionary[a].final = 0;
dictionary[a].arc = 0;
if (prev == arc) {
dictionary[a].next = dictionary[prev].arc;
dictionary[prev].arc = a;
} else {
dictionary[a].next = dictionary[prev].next;
dictionary[prev].next = a;
}
return a;
}
static int validateword(char *word)
{
int i;
for (i = 0 ; word[i] != '\0' && word[i] != '\n' ; ++i)
if (word[i] < 'a' || word[i] > 'z')
return 0;
if (word[i] == '\n')
word[i] = '\0';
if (i < 3)
return 0;
for ( ; *word ; ++word, --i) {
if (*word == 'q') {
if (word[1] != 'u')
return 0;
memmove(word + 1, word + 2, --i);
}
}
return 1;
}
static void createdictionary(char const *filename)
{
FILE *fp;
int arc, i;
initdictionary();
fp = fopen(filename, "r");
while (fgets(wordbuf, sizeof wordbuf, fp)) {
if (!validateword(wordbuf))
continue;
arc = 0;
for (i = 0 ; wordbuf[i] ; ++i)
arc = addarc(arc, wordbuf[i]);
dictionary[arc].final = 1;
}
fclose(fp);
}
/*
* The wordlist is stored as a binary search tree. It is only added
* to, searched, and erased. Instead of storing the actual word, it
* only retains the word's final arc in the dictionary. Thus, the
* dictionary needs to be walked in order to print out the wordlist.
*/
static void initwordlist(void)
{
listalloc = 16;
wordlist = malloc(listalloc * sizeof *wordlist);
listsize = 0;
}
static int iswordinlist(int word)
{
int node, n;
n = 0;
for (;;) {
node = n;
if (wordlist[node].item == word)
return 1;
if (wordlist[node].item > word)
n = wordlist[node].left;
else
n = wordlist[node].right;
if (!n)
return 0;
}
}
static int insertword(int word)
{
int node, n;
if (!listsize) {
wordlist->item = word;
wordlist->left = 0;
wordlist->right = 0;
++listsize;
return 1;
}
n = 0;
for (;;) {
node = n;
if (wordlist[node].item == word)
return 0;
if (wordlist[node].item > word)
n = wordlist[node].left;
else
n = wordlist[node].right;
if (!n)
break;
}
if (listsize >= listalloc) {
listalloc *= 2;
wordlist = realloc(wordlist, listalloc * sizeof *wordlist);
}
n = listsize++;
wordlist[n].item = word;
wordlist[n].left = 0;
wordlist[n].right = 0;
if (wordlist[node].item > word)
wordlist[node].left = n;
else
wordlist[node].right = n;
return 1;
}
static void clearwordlist(void)
{
listsize = 0;
G.score = 0;
}
static void scoreword(char const *word)
{
int const scoring[] = { 0, 0, 0, 1, 1, 2, 3, 5 };
int n, u;
for (n = u = 0 ; word[n] ; ++n)
if (word[n] == 'q')
++u;
n += u;
G.score += n > 7 ? 11 : scoring[n];
}
static void addwordtolist(char const *word, int id)
{
if (insertword(id))
scoreword(word);
}
static void _printwords(int arc, int len)
{
int a;
while (arc) {
a = len + 1;
wordbuf[len] = dictionary[arc].letter;
if (wordbuf[len] == 'q')
wordbuf[a++] = 'u';
if (dictionary[arc].final) {
if (iswordinlist(arc)) {
wordbuf[a] = '\0';
if (xcursor == 4) {
printf("%s\n", wordbuf);
xcursor = 0;
} else {
printf("%-16s", wordbuf);
++xcursor;
}
}
}
_printwords(dictionary[arc].arc, a);
arc = dictionary[arc].next;
}
}
static void printwordlist(void)
{
xcursor = 0;
_printwords(1, 0);
if (xcursor)
putchar('\n');
}
/*
* The board is stored as an array of oriented dice. To score a game,
* the program looks at each slot on the board in turn, and tries to
* find a path along the dictionary tree that matches the letters on
* adjacent dice.
*/
static void initneighbors(void)
{
int i, j, n;
for (i = 0 ; i < BOARDSIZE ; ++i) {
n = 0;
for (j = 0 ; j < BOARDSIZE ; ++j)
if (i != j && abs(i / XSIZE - j / XSIZE) <= 1
&& abs(i % XSIZE - j % XSIZE) <= 1)
neighbors[i][n++] = j;
neighbors[i][n] = -1;
}
}
static void printboard(void)
{
int i;
for (i = 0 ; i < BOARDSIZE ; ++i) {
printf(" %c", toupper(dice[G.board[i].die][G.board[i].face]));
if (i % XSIZE == XSIZE - 1)
putchar('\n');
}
}
static void _findwords(int pos, int arc, int len)
{
int ch, i, p;
for (;;) {
ch = dictionary[arc].letter;
if (ch == gridbuf[pos])
break;
if (ch > gridbuf[pos] || !dictionary[arc].next)
return;
arc = dictionary[arc].next;
}
wordbuf[len++] = ch;
if (dictionary[arc].final) {
wordbuf[len] = '\0';
addwordtolist(wordbuf, arc);
}
gridbuf[pos] = '.';
for (i = 0 ; (p = neighbors[pos][i]) >= 0 ; ++i)
if (gridbuf[p] != '.')
_findwords(p, dictionary[arc].arc, len);
gridbuf[pos] = ch;
}
static void findwordsingrid(void)
{
int i;
clearwordlist();
for (i = 0 ; i < BOARDSIZE ; ++i)
gridbuf[i] = dice[G.board[i].die][G.board[i].face];
for (i = 0 ; i < BOARDSIZE ; ++i)
_findwords(i, 1, 0);
}
static void shuffleboard(void)
{
int die[BOARDSIZE];
int i, n;
for (i = 0 ; i < BOARDSIZE ; ++i)
die[i] = i;
for (i = BOARDSIZE ; i-- ; ) {
n = random(i);
G.board[i].die = die[n];
G.board[i].face = random(DIEFACES);
die[n] = die[i];
}
}
/*
* The pool contains the N highest-scoring games found so far. (This
* would typically be done using a priority queue, but it represents
* far too little of the runtime. Brute force is just as good and
* simpler.) Note that the pool will only ever contain one board with
* a particular score: This is a cheap way to discourage the pool from
* filling up with almost-identical high-scoring boards.
*/
static void addgametopool(void)
{
int i;
if (G.score < cutoffscore)
return;
for (i = 0 ; i < poolsize ; ++i) {
if (G.score == pool[i].score) {
pool[i] = G;
return;
}
if (G.score > pool[i].score)
break;
}
if (poolsize < MAXPOOLSIZE)
++poolsize;
if (i < poolsize) {
memmove(pool + i + 1, pool + i, (poolsize - i - 1) * sizeof *pool);
pool[i] = G;
}
cutoffscore = pool[poolsize - 1].score;
stallcounter = 0;
}
static void selectpoolmember(int n)
{
G = pool[n];
}
static void emptypool(void)
{
poolsize = 0;
cutoffscore = 0;
stallcounter = 0;
}
/*
* The program examines as many boards as it can in the given time,
* and retains the one with the highest score. If the program is out
* of time, then it reports the best-seen game and immediately exits.
*/
static void report(void)
{
findwordsingrid();
printboard();
printwordlist();
printf("score = %d\n", G.score);
fprintf(stderr, "// score: %d points (%d words)\n", G.score, listsize);
fprintf(stderr, "// %d boards examined\n", boardcount);
fprintf(stderr, "// avg score: %.1f\n", (double)allscores / boardcount);
fprintf(stderr, "// runtime: %ld s\n", time(0) - start);
}
static void scoreboard(void)
{
findwordsingrid();
++boardcount;
allscores += G.score;
addgametopool();
if (bestgame.score < G.score) {
bestgame = G;
fprintf(stderr, "// %ld s: board %d scoring %d\n",
time(0) - start, boardcount, G.score);
}
if (time(0) - start >= RUNTIME) {
G = bestgame;
report();
exit(0);
}
}
static void restartpool(void)
{
emptypool();
while (poolsize < MAXPOOLSIZE) {
shuffleboard();
scoreboard();
}
}
/*
* Making small modifications to a board.
*/
static void turndie(void)
{
int i, j;
i = random(BOARDSIZE);
j = random(DIEFACES - 1) + 1;
G.board[i].face = (G.board[i].face + j) % DIEFACES;
}
static void swapdice(void)
{
slot t;
int p, q;
p = random(BOARDSIZE);
q = random(BOARDSIZE - 1);
if (q >= p)
++q;
t = G.board[p];
G.board[p] = G.board[q];
G.board[q] = t;
}
/*
*
*/
int main(void)
{
int i;
start = time(0);
srand((unsigned int)start);
createdictionary(WORDLISTFILE);
initwordlist();
initneighbors();
restartpool();
for (;;) {
for (i = 0 ; i < poolsize ; ++i) {
selectpoolmember(i);
turndie();
scoreboard();
selectpoolmember(i);
swapdice();
scoreboard();
}
++stallcounter;
if (stallcounter >= STALLPOINT) {
fprintf(stderr, "// stalled; restarting search\n");
restartpool();
}
}
return 0;
}
Note per la versione 2 (9 giugno)
Ecco un modo per utilizzare la versione iniziale del mio codice come punto di partenza. Le modifiche a questa versione consistono in meno di 100 righe, ma triplicano il punteggio medio del gioco.
In questa versione, il programma mantiene un "pool" di candidati, composto dalle N commissioni con il punteggio più alto che il programma ha generato finora. Ogni volta che viene generata una nuova scheda, questa viene aggiunta al pool e viene rimossa la scheda con il punteggio più basso nel pool (che potrebbe benissimo essere la board appena aggiunta, se il suo punteggio è inferiore a quello che è già presente). Il pool viene inizialmente riempito con schede generate casualmente, dopo di che mantiene una dimensione costante durante l'esecuzione del programma.
Il ciclo principale del programma consiste nel selezionare una tavola a caso dal pool e modificarla, determinando il punteggio di questa nuova tavola e poi inserendola nel pool (se segna abbastanza bene). In questo modo il programma perfeziona continuamente le schede ad alto punteggio. L'attività principale è apportare miglioramenti graduali e incrementali, ma la dimensione del pool consente anche al programma di trovare miglioramenti in più passaggi che peggiorano temporaneamente il punteggio di una scheda prima che possa migliorarla.
In genere questo programma trova un buon massimo locale piuttosto rapidamente, dopo il quale presumibilmente un massimo migliore è troppo distante per essere trovato. E quindi, ancora una volta, non ha senso eseguire il programma per più di 10 secondi. Ciò potrebbe essere migliorato, ad esempio facendo in modo che il programma rilevi questa situazione e inizi una nuova ricerca con un nuovo pool di candidati. Tuttavia, ciò comporterebbe solo un aumento marginale. Una corretta tecnica di ricerca euristica sarebbe probabilmente una migliore via di esplorazione.
(Nota a margine: ho visto che questa versione generava circa 5k board / sec. Dato che la prima versione in genere faceva 20k board / sec, inizialmente ero preoccupato. Alla profilazione, tuttavia, ho scoperto che il tempo extra è stato impiegato nella gestione dell'elenco di parole. In altre parole, è stato interamente dovuto al fatto che il programma ha trovato molte più parole per scheda. Alla luce di ciò, ho considerato di cambiare il codice per gestire l'elenco di parole, ma dato che questo programma utilizza solo 10 dei 120 secondi assegnati, ad esempio un'ottimizzazione sarebbe molto prematura.)
Note per la versione 1 (2 giugno)
Questa è una soluzione molto, molto semplice. Tutto ciò che genera schede casuali, e dopo 10 secondi emette quello con il punteggio più alto. (Ho impostato automaticamente 10 secondi perché i 110 secondi extra consentiti dalla specifica del problema in genere non migliorano la soluzione finale trovata abbastanza per cui vale la pena aspettare.) Quindi è estremamente stupida. Tuttavia, ha tutte le infrastrutture per creare un buon punto di partenza per una ricerca più intelligente e, se qualcuno desidera utilizzarlo prima della scadenza, li incoraggio a farlo.
Il programma inizia leggendo il dizionario in una struttura ad albero. (Il modulo non è così ottimizzato come potrebbe essere, ma è più che sufficiente per questi scopi.) Dopo qualche altra inizializzazione di base, inizia quindi a generare schede e assegnarle un punteggio. Il programma esamina circa 20.000 schede al secondo sulla mia macchina e dopo circa 200.000 schede l'approccio casuale inizia a funzionare a secco.
Dal momento che solo una scheda viene attualmente valutata in un dato momento, i dati del punteggio vengono archiviati in variabili globali. Ciò mi consente di ridurre al minimo la quantità di dati costanti che devono essere passati come argomenti alle funzioni ricorsive. (Sono sicuro che questo darà ad alcuni alveari, e a loro mi scuso.) La lista di parole è memorizzata come un albero di ricerca binario. Ogni parola trovata deve essere cercata nell'elenco delle parole, in modo che le parole duplicate non vengano conteggiate due volte. L'elenco di parole è necessario solo durante il processo di evaulazione, quindi viene scartato dopo aver trovato il punteggio. Pertanto, alla fine del programma, la tavola scelta deve essere nuovamente classificata, in modo che l'elenco di parole possa essere stampato.
Curiosità: il punteggio medio per una tavola Boggle generata casualmente, come segnato da english.0
, è di 61,7 punti.
4527
(1414
parole in totale), trovato qui: ai.stanford.edu/~chuongdo/boggle/index.html