C, promediando 500+ 1500 1750 puntos
Esta es una mejora relativamente menor con respecto a la versión 2 (ver abajo las notas sobre versiones anteriores). Hay dos partes Primero: en lugar de seleccionar tableros al azar del grupo, el programa ahora itera sobre cada tablero en el grupo, usando cada uno por turno antes de regresar a la parte superior del grupo y repetir. (Dado que el grupo se modifica mientras se produce esta iteración, todavía habrá paneles que se eligen dos veces seguidas, o peor, pero esto no es una preocupación seria). El segundo cambio es que el programa ahora rastrea cuando el grupo cambia , y si el programa dura demasiado tiempo sin mejorar el contenido del grupo, determina que la búsqueda se ha "estancado", vacía el grupo y comienza de nuevo con una nueva búsqueda. Continúa haciendo esto hasta que hayan transcurrido los dos minutos.
Al principio pensé que estaría empleando algún tipo de búsqueda heurística para superar el rango de 1500 puntos. El comentario de @ mellamokb sobre un tablero de 4527 puntos me llevó a suponer que había mucho margen de mejora. Sin embargo, estamos usando una lista de palabras relativamente pequeña. El tablero de 4527 puntos estaba anotando usando YAWL, que es la lista de palabras más inclusiva que existe, es incluso más grande que la lista de palabras oficial de Scrabble de EE. UU. Con esto en mente, volví a examinar los tableros que mi programa había encontrado y noté que parecía haber un conjunto limitado de tableros por encima de 1700 más o menos. Entonces, por ejemplo, tuve varias carreras que descubrieron un tablero con 1726, pero siempre fue el mismo tablero que se encontró (ignorando rotaciones y reflexiones).
Como otra prueba, ejecuté mi programa usando YAWL como diccionario, y encontré el tablero de 4527 puntos después de aproximadamente una docena de carreras. Dado esto, estoy planteando la hipótesis de que mi programa ya está en el límite superior del espacio de búsqueda y, por lo tanto, la reescritura que estaba planeando introduciría una complejidad adicional para muy poca ganancia.
Aquí está mi lista de los cinco tableros de mayor puntaje que mi programa ha encontrado usando la english.0
lista de palabras:
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
Creo que el "tablero grep" de 1772 (como lo he llamado), con 531 palabras, es el tablero con la puntuación más alta posible con esta lista de palabras. Más del 50% de las ejecuciones de dos minutos de mi programa terminan con esta placa. También dejé mi programa ejecutándose durante la noche sin encontrar nada mejor. Entonces, si hay un tablero con una puntuación más alta, es probable que tenga algún aspecto que derrote la técnica de búsqueda del programa. Un tablero en el que cada pequeño cambio posible en el diseño causa una gran caída en el puntaje total, por ejemplo, nunca podría ser descubierto por mi programa. Mi presentimiento es que es muy poco probable que exista tal tablero.
#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;
}
Notas para la versión 2 (9 de junio)
Aquí hay una forma de usar la versión inicial de mi código como punto de partida. Los cambios a esta versión consisten en menos de 100 líneas, pero triplicaron el puntaje promedio del juego.
En esta versión, el programa mantiene un "grupo" de candidatos, que consta de los N tableros con la puntuación más alta que el programa ha generado hasta ahora. Cada vez que se genera un nuevo tablero, se agrega al grupo y se elimina el tablero con el puntaje más bajo en el grupo (que muy bien podría ser el tablero que se acaba de agregar, si su puntaje es más bajo que el que ya está allí). El grupo se llena inicialmente con paneles generados aleatoriamente, después de lo cual mantiene un tamaño constante durante la ejecución del programa.
El ciclo principal del programa consiste en seleccionar un tablero aleatorio del grupo y modificarlo, determinar el puntaje de este nuevo tablero y luego colocarlo en el grupo (si obtiene un puntaje suficiente). De esta manera, el programa refina continuamente tableros de alto puntaje. La actividad principal es realizar mejoras graduales e incrementales, pero el tamaño de la agrupación también permite que el programa encuentre mejoras de varios pasos que empeoran temporalmente el puntaje de un tablero antes de que pueda mejorarlo.
Por lo general, este programa encuentra un buen máximo local con bastante rapidez, después de lo cual, presumiblemente, cualquier máximo mejor es demasiado distante para ser encontrado. Y así, una vez más, no tiene mucho sentido ejecutar el programa durante más de 10 segundos. Esto podría mejorarse, por ejemplo, haciendo que el programa detecte esta situación y comience una nueva búsqueda con un nuevo grupo de candidatos. Sin embargo, esto generaría solo un aumento marginal. Una técnica de búsqueda heurística adecuada probablemente sería una mejor vía de exploración.
(Nota al margen: vi que esta versión estaba generando alrededor de 5k tableros / seg. Como la primera versión típicamente producía 20k tableros / seg, al principio me preocupé. Sin embargo, al hacer un perfil, descubrí que el tiempo adicional se dedicó a administrar la lista de palabras. En otras palabras, se debió por completo al programa de encontrar muchas más palabras por placa. A la luz de esto, consideré cambiar el código para administrar la lista de palabras, pero dado que este programa solo usa 10 de sus 120 segundos asignados, tal una optimización sería muy prematura).
Notas para la versión 1 (2 de junio)
Esta es una solución muy, muy simple. Todo lo que hace es generar tableros aleatorios, y luego de 10 segundos genera el que tiene la puntuación más alta. (El valor predeterminado fue de 10 segundos porque los 110 segundos adicionales permitidos por la especificación del problema generalmente no mejoran la solución final encontrada lo suficiente como para que valga la pena esperar). Por lo tanto, es extremadamente tonto. Sin embargo, tiene toda la infraestructura para hacer un buen punto de partida para una búsqueda más inteligente, y si alguien desea utilizarla antes de la fecha límite, les animo a que lo hagan.
El programa comienza leyendo el diccionario en una estructura de árbol. (El formulario no está tan optimizado como podría estarlo, pero es más que suficiente para estos fines). Después de alguna otra inicialización básica, comienza a generar tablas y puntuarlas. El programa examina aproximadamente 20k tableros por segundo en mi máquina, y después de aproximadamente 200k tableros, el enfoque aleatorio comienza a funcionar en seco.
Como solo se está evaluando un tablero en un momento dado, los datos de puntuación se almacenan en variables globales. Esto me permite minimizar la cantidad de datos constantes que deben pasarse como argumentos a las funciones recursivas. (Estoy seguro de que esto les dará colmenas a algunas personas, y les pido disculpas). La lista de palabras se almacena como un árbol de búsqueda binario. Cada palabra encontrada debe buscarse en la lista de palabras, para que las palabras duplicadas no se cuenten dos veces. Sin embargo, la lista de palabras solo es necesaria durante el proceso de evacuación, por lo que se descarta después de encontrar el puntaje. Por lo tanto, al final del programa, la tabla elegida debe puntuarse nuevamente para que la lista de palabras se pueda imprimir.
Dato curioso: el puntaje promedio para un tablero Boggle generado aleatoriamente, según la puntuación english.0
, es 61.7 puntos.
4527
(1414
palabras totales), que se encuentra aquí: ai.stanford.edu/~chuongdo/boggle/index.html