С, в среднем 500+ 1500 1750 баллов
Это относительно небольшое улучшение по сравнению с версией 2 (см. Примечания к предыдущим версиям ниже). Есть две части. Во-первых: вместо случайного выбора досок из пула, программа теперь выполняет итерации по каждой доске в пуле, используя каждую из них по очереди, прежде чем вернуться к вершине пула и повторить. (Так как пул изменяется во время этой итерации, все еще будут платы, которые выбираются дважды подряд, или хуже, но это не представляет серьезной проблемы.) Второе изменение заключается в том, что программа теперь отслеживает изменения пула и если программа выполняется слишком долго, не улучшая содержимое пула, она определяет, что поиск "остановился", очищает пул и начинает заново с новым поиском. Он продолжает делать это, пока две минуты не истекут.
Сначала я думал, что буду использовать какой-то эвристический поиск, чтобы выйти за пределы диапазона в 1500 пунктов. Комментарий @ mellamokb о доске из 4527 пунктов заставил меня предположить, что есть много возможностей для улучшения. Однако мы используем сравнительно небольшой список слов. Доска из 4527 баллов набирала баллы с использованием YAWL, который является самым всеобъемлющим из всех списков слов - он даже больше, чем официальный список слов США Scrabble. Имея это в виду, я повторно проверил платы, которые обнаружила моя программа, и заметил, что существует ограниченный набор плат выше 1700 или около того. Так, например, у меня было несколько прогонов, в которых была обнаружена доска, набравшая 1726 очков, но это всегда была та же самая доска, которая была найдена (игнорируя повороты и отражения).
В качестве еще одного теста я запустил свою программу, используя YAWL в качестве словаря, и он нашел доску из 4527 баллов после примерно десятка запусков. Учитывая это, я предполагаю, что моя программа уже находится на верхнем пределе пространства поиска, и, следовательно, переписывание, которое я планировал, внесло бы дополнительную сложность при очень небольшом выигрыше.
Вот мой список пяти досок с наибольшим количеством очков, которые моя программа нашла, используя список english.0
слов:
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
Я считаю, что «доска объявлений grep» 1772 года (как я ее назвал), состоящая из 531 слова, является самой высокой оценочной доской, возможной для этого списка слов. Более 50% двухминутных прогонов моей программы заканчиваются на этой доске. Я также оставил свою программу работающей на ночь, но не нашел ничего лучшего. Так что, если есть доска с более высоким баллом, она, вероятно, должна иметь какой-то аспект, который побеждает технику поиска программы. Например, моя программа никогда не сможет обнаружить доску, на которой каждое возможное небольшое изменение макета приводит к значительному снижению общего балла. Я догадываюсь, что такая доска вряд ли существует.
#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;
}
Примечания к версии 2 (9 июня)
Вот один из способов использовать начальную версию моего кода в качестве отправной точки. Изменения в этой версии состоят из менее чем 100 строк, но в три раза увеличивают средний игровой счет.
В этой версии программа поддерживает «пул» кандидатов, состоящий из N досок с наибольшим количеством очков, которые программа сгенерировала до сих пор. Каждый раз, когда генерируется новая доска, она добавляется в пул, а доска с наименьшим количеством очков в пуле удаляется (что вполне может быть только что добавленной доской, если ее оценка ниже, чем у того, что уже есть). Изначально пул заполняется случайно сгенерированными досками, после чего он сохраняет постоянный размер на протяжении всего выполнения программы.
Основной цикл программы состоит в том, чтобы выбрать случайную доску из пула и изменить ее, определить счет этой новой доски и затем поместить ее в пул (если она набирает достаточно хорошие оценки). Таким образом, программа постоянно совершенствует доски с высокими баллами. Основным действием является постепенное, поэтапное улучшение, но размер пула также позволяет программе находить многоэтапные улучшения, которые временно ухудшают счет доски, прежде чем она улучшится.
Как правило, эта программа довольно быстро находит хороший локальный максимум, после которого, по-видимому, любой лучший максимум находится слишком далеко, чтобы его можно было найти. И опять же, нет смысла запускать программу дольше 10 секунд. Это может быть улучшено, например, если программа обнаружит эту ситуацию и начнет новый поиск с новым пулом кандидатов. Однако это приведет лишь к незначительному увеличению. Правильная техника эвристического поиска, вероятно, будет лучшим способом исследования.
(Примечание: я видел, что эта версия генерирует около 5 тыс. Досок в секунду. Поскольку первая версия обычно производила 20 тыс. Досок в секунду, я был изначально обеспокоен. Однако после профилирования я обнаружил, что на управление списком слов было потрачено дополнительное время. Другими словами, это было полностью связано с тем, что программа находила намного больше слов на доске. В свете этого я рассмотрел вопрос об изменении кода для управления списком слов, но, учитывая, что эта программа использует только 10 из отведенных ей 120 секунд, например, оптимизация была бы очень преждевременной.)
Примечания к версии 1 (2 июня)
Это очень, очень простое решение. Все, что он делает, это генерирует случайные доски, а затем через 10 секунд выводит ту, которая набрала наибольшее количество очков. (Я установил значение по умолчанию на 10 секунд, потому что дополнительные 110 секунд, разрешенные спецификацией проблемы, как правило, не улучшают окончательное решение, найденное достаточно, чтобы его стоило ждать.) Так что это чрезвычайно глупо. Тем не менее, он обладает всей инфраструктурой, чтобы стать хорошей отправной точкой для более интеллектуального поиска, и если кто-то захочет использовать его до истечения срока, я призываю его сделать это.
Программа начинается с чтения словаря в древовидную структуру. (Форма не настолько оптимизирована, как могла бы быть, но она более чем хороша для этих целей.) После некоторой другой базовой инициализации она затем начинает генерировать платы и оценивать их. Программа проверяет около 20 тысяч досок в секунду на моей машине, и после примерно 200 тысяч досок случайный подход начинает иссякать.
Поскольку в каждый момент времени оценивается только одна доска, данные оценки сохраняются в глобальных переменных. Это позволяет мне минимизировать количество постоянных данных, которые должны передаваться в качестве аргументов рекурсивным функциям. (Я уверен, что это даст некоторым людям ульи, и им я приношу свои извинения.) Список слов хранится в виде двоичного дерева поиска. Каждое найденное слово должно быть найдено в списке слов, чтобы повторяющиеся слова не учитывались дважды. Однако список слов необходим только во время процесса эвакуации, поэтому он отбрасывается после того, как результат будет найден. Таким образом, в конце программы выбранная доска должна быть оценена заново, чтобы можно было распечатать список слов.
Забавный факт: средний балл за случайно сгенерированную доску Boggle, по english.0
оценкам, составляет 61,7 балла.
4527
(1414
всего слов), найдена здесь: ai.stanford.edu/~chuongdo/boggle/index.html