C, в середньому 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
Я вважаю, що "греп-борд" 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