C, en moyenne 500+ 1500 1750 points
Il s'agit d'une amélioration relativement mineure par rapport à la version 2 (voir ci-dessous pour les notes sur les versions précédentes). Il y a deux parties. Premièrement: au lieu de sélectionner des tableaux au hasard dans le pool, le programme itère maintenant sur chaque tableau du pool, en les utilisant à tour de rôle avant de retourner en haut du pool et de répéter. (Étant donné que le pool est en cours de modification pendant cette itération, il y aura toujours des cartes qui seront choisies deux fois de suite, ou pire, mais ce n'est pas un problème grave.) Le deuxième changement est que le programme suit maintenant lorsque le pool change et si le programme dure trop longtemps sans améliorer le contenu du pool, il détermine que la recherche est "bloquée", vide le pool et recommence avec une nouvelle recherche. Il continue de le faire jusqu'à ce que les deux minutes soient écoulées.
J'avais initialement pensé que j'utiliserais une sorte de recherche heuristique pour dépasser la plage des 1500 points. Le commentaire de @ mellamokb à propos d'un tableau de 4527 points m'a amené à supposer qu'il y avait beaucoup de place pour l'amélioration. Cependant, nous utilisons une liste de mots relativement petite. Le tableau de 4527 points marquait en utilisant YAWL, qui est la liste de mots la plus inclusive - elle est encore plus grande que la liste de mots officielle du Scrabble américain. Dans cet esprit, j'ai réexaminé les planches que mon programme avait trouvées et j'ai remarqué qu'il semblait y avoir un ensemble limité de planches au-dessus de 1700 environ. Ainsi, par exemple, j'ai eu plusieurs pistes qui avaient découvert une planche marquant 1726, mais c'était toujours exactement la même planche que celle trouvée (en ignorant les rotations et les réflexions).
Comme autre test, j'ai exécuté mon programme en utilisant YAWL comme dictionnaire, et il a trouvé la carte à 4527 points après environ une douzaine d'exécutions. Compte tenu de cela, je fais l'hypothèse que mon programme est déjà à la limite supérieure de l'espace de recherche, et donc la réécriture que je prévoyais introduirait une complexité supplémentaire pour très peu de gain.
Voici ma liste des cinq tableaux les mieux notés que mon programme a trouvés en utilisant la liste de english.0
mots:
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
Ma conviction est que le 1772 "grep board" (comme je l'ai pris pour l'appeler), avec 531 mots, est le tableau le plus performant possible avec cette liste de mots. Plus de 50% des parcours de deux minutes de mon programme se terminent avec ce forum. J'ai également laissé mon programme fonctionner pendant la nuit sans qu'il ne trouve rien de mieux. Donc, s'il existe un tableau avec un score plus élevé, il devrait probablement avoir un aspect qui vainc la technique de recherche du programme. Un tableau dans lequel chaque petite modification possible de la mise en page provoque une énorme baisse du score total, par exemple, pourrait ne jamais être découvert par mon programme. Mon intuition est qu'il est très peu probable qu'un tel conseil existe.
#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;
}
Notes pour la version 2 (9 juin)
Voici une façon d'utiliser la version initiale de mon code comme point de départ. Les modifications apportées à cette version se composent de moins de 100 lignes, mais ont triplé le score de jeu moyen.
Dans cette version, le programme maintient un "pool" de candidats, composé des N tableaux les mieux notés que le programme a généré jusqu'à présent. Chaque fois qu'un nouveau tableau est généré, il est ajouté au pool et le tableau le moins bien noté dans le pool est supprimé (qui peut très bien être le tableau qui vient d'être ajouté, si son score est inférieur à celui qui existe déjà). Le pool est initialement rempli de cartes générées aléatoirement, après quoi il conserve une taille constante tout au long du programme.
La boucle principale du programme consiste à sélectionner un tableau aléatoire dans le pool et à le modifier, à déterminer le score de ce nouveau tableau, puis à le placer dans le pool (s'il réussit assez bien). De cette façon, le programme perfectionne continuellement les tableaux à score élevé. L'activité principale consiste à apporter des améliorations progressives et incrémentielles, mais la taille du pool permet également au programme de trouver des améliorations en plusieurs étapes qui aggravent temporairement le score d'un conseil avant de pouvoir l'améliorer.
Généralement, ce programme trouve un bon maximum local assez rapidement, après quoi un meilleur maximum est probablement trop éloigné pour être trouvé. Et donc, encore une fois, il est inutile d'exécuter le programme pendant plus de 10 secondes. Cela pourrait être amélioré, par exemple en demandant au programme de détecter cette situation et de lancer une nouvelle recherche avec un nouveau groupe de candidats. Cependant, cela ne représenterait qu'une augmentation marginale. Une technique de recherche heuristique appropriée serait probablement une meilleure voie d'exploration.
(Note latérale: j'ai vu que cette version générait environ 5 000 tableaux / seconde. Comme la première version faisait généralement 20 000 tableaux / seconde, j'étais initialement inquiet. Cependant, lors du profilage, j'ai découvert que le temps supplémentaire était consacré à la gestion de la liste de mots. En d'autres termes, cela est entièrement dû au fait que le programme a trouvé beaucoup plus de mots par tableau. À la lumière de cela, j'ai envisagé de changer le code pour gérer la liste de mots, mais étant donné que ce programme n'utilise que 10 des 120 secondes allouées, telles que une optimisation serait très prématurée.)
Notes pour la version 1 (2 juin)
Il s'agit d'une solution très, très simple. Tout ce qu'il fait, il génère des tableaux aléatoires, puis après 10 secondes, il sort celui avec le score le plus élevé. (Je suis passé par défaut à 10 secondes car les 110 secondes supplémentaires autorisées par la spécification du problème n'améliorent généralement pas la solution finale trouvée suffisamment pour mériter d'être attendue.) C'est donc extrêmement stupide. Cependant, il dispose de toutes les infrastructures nécessaires pour constituer un bon point de départ pour une recherche plus intelligente, et si quelqu'un souhaite en faire usage avant la date limite, je les encourage à le faire.
Le programme commence par lire le dictionnaire dans une arborescence. (Le formulaire n'est pas aussi optimisé qu'il pourrait l'être, mais il est plus que suffisant pour ces fins.) Après une autre initialisation de base, il commence alors à générer des tableaux et à les noter. Le programme examine environ 20 000 planches par seconde sur ma machine, et après environ 200 000 planches, l'approche aléatoire commence à tourner à sec.
Puisqu'une seule carte est réellement évaluée à un moment donné, les données de notation sont stockées dans des variables globales. Cela me permet de minimiser la quantité de données constantes qui doivent être transmises comme arguments aux fonctions récursives. (Je suis sûr que cela donnera des ruches à certaines personnes, et je m'excuse pour elles.) La liste de mots est stockée sous forme d'arbre de recherche binaire. Chaque mot trouvé doit être recherché dans la liste de mots, afin que les mots en double ne soient pas comptés deux fois. La liste de mots n'est nécessaire que pendant le processus d'évaluation, elle est donc supprimée une fois la partition trouvée. Ainsi, à la fin du programme, le tableau choisi doit être noté à nouveau, afin que la liste de mots puisse être imprimée.
Fait amusant: Le score moyen pour un tableau Boggle généré de manière aléatoire, tel que marqué par english.0
, est de 61,7 points.
4527
(1414
nombre total de mots), trouvé ici: ai.stanford.edu/~chuongdo/boggle/index.html