C, im Durchschnitt mehr als 1500 1750 Punkte
Dies ist eine relativ geringfügige Verbesserung gegenüber Version 2 (siehe unten für Hinweise zu früheren Versionen). Es gibt zwei Teile. Erstens: Anstatt Bretter zufällig aus dem Pool auszuwählen, durchläuft das Programm nun alle Bretter im Pool und verwendet sie nacheinander, bevor es zum oberen Rand des Pools zurückkehrt und sie wiederholt. (Da der Pool während dieser Iteration geändert wird, gibt es immer noch Boards, die zweimal hintereinander oder noch schlimmer ausgewählt werden. Dies ist jedoch kein ernstes Problem.) Die zweite Änderung besteht darin, dass das Programm jetzt nachverfolgt, wann sich der Pool ändert , und wenn das Programm zu lange läuft, ohne den Poolinhalt zu verbessern, wird festgestellt, dass die Suche "angehalten" hat, der Pool geleert wird und eine neue Suche gestartet wird. Dies wird fortgesetzt, bis die zwei Minuten abgelaufen sind.
Ich hatte ursprünglich gedacht, dass ich eine Art heuristische Suche durchführen würde, um über den 1500-Punkte-Bereich hinauszukommen. @ mellamokbs Kommentar zu einem 4527-Punkte-Board hat mich zu der Annahme veranlasst, dass es viel Raum für Verbesserungen gibt. Wir verwenden jedoch eine relativ kleine Wortliste. Das 4527-Punkte-Board bewertete mit YAWL, der umfassendsten Wortliste auf dem Markt - sie ist sogar größer als die offizielle US-Scrabble-Wortliste. In diesem Sinne überprüfte ich die Boards, die mein Programm gefunden hatte, erneut und stellte fest, dass es eine begrenzte Anzahl von Boards über 1700 zu geben schien. So hatte ich zum Beispiel mehrere Läufe, bei denen ein Board gefunden wurde, das 1726 Punkte erzielte, aber es wurde immer genau dasselbe Board gefunden (ohne Berücksichtigung von Rotationen und Reflexionen).
Als weiteren Test habe ich mein Programm mit YAWL als Wörterbuch ausgeführt und nach etwa einem Dutzend Durchläufen die 4527-Punkte-Karte gefunden. Angesichts dessen gehe ich davon aus, dass sich mein Programm bereits an der oberen Grenze des Suchbereichs befindet und daher die geplante Neufassung zusätzliche Komplexität mit sehr geringem Gewinn einbringen würde.
Hier ist meine Liste der fünf am english.0
besten bewerteten Boards, die mein Programm anhand der Wortliste gefunden hat:
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
Meiner Meinung nach ist das "Grep-Board" von 1772 (wie ich es nenne) mit 531 Wörtern das Board mit der höchsten Punktzahl, das mit dieser Wortliste möglich ist. Über 50% der zweiminütigen Läufe meines Programms enden mit diesem Board. Ich habe mein Programm auch über Nacht laufen lassen, ohne dass es etwas Besseres findet. Wenn es also ein Board mit höherer Punktzahl gibt, muss es wahrscheinlich einen Aspekt haben, der die Suchtechnik des Programms zunichte macht. Ein Board, bei dem jede mögliche kleine Änderung des Layouts zum Beispiel zu einem großen Rückgang der Gesamtpunktzahl führt, könnte von meinem Programm möglicherweise nie entdeckt werden. Meiner Meinung nach ist es sehr unwahrscheinlich, dass ein solches Board existiert.
#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;
}
Anmerkungen zu Version 2 (9. Juni)
Hier ist eine Möglichkeit, die ursprüngliche Version meines Codes als Ausgangspunkt zu verwenden. Die Änderungen an dieser Version bestehen aus weniger als 100 Zeilen, haben jedoch die durchschnittliche Spielpunktzahl verdreifacht.
In dieser Version verwaltet das Programm einen "Pool" von Kandidaten, bestehend aus den N Boards mit der höchsten Punktzahl, die das Programm bisher generiert hat. Jedes Mal, wenn ein neues Board erstellt wird, wird es dem Pool hinzugefügt und das Board mit der niedrigsten Punktzahl aus dem Pool entfernt (dies kann sehr gut das Board sein, das gerade hinzugefügt wurde, wenn seine Punktzahl niedriger ist als das, was bereits vorhanden ist). Der Pool wird anfänglich mit zufällig generierten Boards gefüllt, wonach er während des gesamten Programmablaufs eine konstante Größe behält.
Die Hauptschleife des Programms besteht darin, eine zufällige Karte aus dem Pool auszuwählen und zu ändern, die Punktzahl dieser neuen Karte zu bestimmen und sie dann in den Pool zu legen (wenn sie gut genug punktet). Auf diese Weise werden die Highscoring-Boards ständig weiterentwickelt. Die Hauptaktivität besteht darin, schrittweise, inkrementelle Verbesserungen vorzunehmen. Die Größe des Pools ermöglicht es dem Programm jedoch auch, Verbesserungen in mehreren Schritten zu finden, die die Punktzahl eines Boards vorübergehend verschlechtern, bevor es sie verbessern kann.
Typischerweise findet dieses Programm ein gutes lokales Maximum ziemlich schnell, wonach vermutlich ein besseres Maximum zu weit entfernt ist, um gefunden zu werden. Und so hat es wieder wenig Sinn, das Programm länger als 10 Sekunden laufen zu lassen. Dies könnte verbessert werden, indem z. B. das Programm diese Situation erkennt und eine neue Suche mit einem neuen Kandidatenpool startet. Dies würde jedoch nur einen geringfügigen Anstieg bedeuten. Eine geeignete heuristische Suchtechnik wäre wahrscheinlich eine bessere Möglichkeit zur Erkundung.
(Randnotiz: Ich sah, dass diese Version ungefähr 5.000 Boards / Sek. Erzeugte. Da die erste Version normalerweise 20.000 Boards / Sek. Erzeugte, war ich anfangs besorgt. Als ich ein Profil erstellte, stellte ich jedoch fest, dass die zusätzliche Zeit für die Verwaltung der Wortliste aufgewendet wurde. Mit anderen Worten, es war ganz darauf zurückzuführen, dass das Programm so viel mehr Wörter pro Board gefunden hat. Aus diesem Grund habe ich überlegt, den Code zu ändern, um die Wortliste zu verwalten, aber da dieses Programm nur 10 seiner zugewiesenen 120 Sekunden verwendet, z Eine Optimierung wäre sehr verfrüht.)
Anmerkungen zu Version 1 (2. Juni)
Dies ist eine sehr, sehr einfache Lösung. Alles, was es macht, sind zufällige Bretter, und nach 10 Sekunden gibt es das mit der höchsten Punktzahl aus. (Ich habe standardmäßig 10 Sekunden verwendet, da die zusätzlichen 110 Sekunden, die in der Problemspezifikation angegeben sind, in der Regel nicht die endgültige Lösung verbessern, auf die es sich zu warten lohnt.) Es ist also extrem dumm. Es verfügt jedoch über die gesamte Infrastruktur, um einen guten Ausgangspunkt für eine intelligentere Suche zu schaffen, und wenn jemand es vor Ablauf der Frist nutzen möchte, ermutige ich ihn, dies zu tun.
Das Programm beginnt mit dem Einlesen des Wörterbuchs in eine Baumstruktur. (Das Formular ist nicht ganz so optimiert wie es sein könnte, aber es ist mehr als gut genug für diese Zwecke.) Nach einer weiteren grundlegenden Initialisierung werden Bretter generiert und bewertet. Das Programm untersucht ungefähr 20.000 Boards pro Sekunde auf meiner Maschine, und nach ungefähr 200.000 Boards beginnt der Zufallsansatz, trocken zu laufen.
Da zu jedem Zeitpunkt nur eine Karte tatsächlich ausgewertet wird, werden die Scoring-Daten in globalen Variablen gespeichert. Dadurch kann ich die Menge der konstanten Daten minimieren, die als Argumente an die rekursiven Funktionen übergeben werden müssen. (Ich bin sicher, dass dies einigen Leuten Bienenstöcke geben wird, und ich entschuldige mich bei ihnen.) Die Wortliste wird als binärer Suchbaum gespeichert. Jedes gefundene Wort muss in der Wortliste nachgeschlagen werden, damit doppelte Wörter nicht doppelt gezählt werden. Die Wortliste wird jedoch nur während des Auswertungsprozesses benötigt, sodass sie gelöscht wird, nachdem die Punktzahl gefunden wurde. Am Ende des Programms muss die gewählte Tafel also noch einmal gewertet werden, damit die Wortliste ausgedruckt werden kann.
Unterhaltsame Tatsache: Die durchschnittliche Punktzahl für ein zufällig generiertes Boggle-Board english.0
beträgt 61,7 Punkte.
4527
(1414
Gesamt Wörter), finden Sie hier: ai.stanford.edu/~chuongdo/boggle/index.html