C, trung bình 500+ 1500 1750 điểm
Đây là một cải tiến tương đối nhỏ so với phiên bản 2 (xem bên dưới để biết ghi chú về các phiên bản trước). Có hai phần. Đầu tiên: Thay vì chọn các bảng ngẫu nhiên từ nhóm, chương trình hiện lặp lại trên mỗi bảng trong nhóm, sử dụng lần lượt từng bảng trước khi quay lại đỉnh của nhóm và lặp lại. (Vì nhóm đang được sửa đổi trong khi lặp lại này xảy ra, vẫn sẽ có các bảng được chọn hai lần liên tiếp hoặc tệ hơn, nhưng đây không phải là vấn đề nghiêm trọng.) Thay đổi thứ hai là chương trình hiện theo dõi khi nhóm thay đổi và nếu chương trình diễn ra quá lâu mà không cải thiện nội dung của nhóm, nó sẽ xác định rằng tìm kiếm đã bị "đình trệ", làm trống hồ bơi và bắt đầu lại với một tìm kiếm mới. Nó tiếp tục làm điều này cho đến khi hai phút là hết.
Ban đầu tôi đã nghĩ rằng tôi sẽ sử dụng một số loại tìm kiếm heuristic để vượt ra ngoài phạm vi 1500 điểm. Nhận xét của @ mellamokb về một bảng 4527 điểm khiến tôi cho rằng có nhiều chỗ để cải thiện. Tuy nhiên, chúng tôi đang sử dụng một danh sách từ tương đối nhỏ. Bảng 4527 điểm đã được chấm điểm bằng YAWL, đây là danh sách từ bao gồm nhiều nhất ngoài đó - nó thậm chí còn lớn hơn cả danh sách từ chính thức của Scrabble Hoa Kỳ. Với suy nghĩ này, tôi đã kiểm tra lại các bảng mà chương trình của tôi đã tìm thấy và nhận thấy rằng dường như có một bộ bảng giới hạn trên 1700 hoặc hơn. Vì vậy, ví dụ, tôi đã có nhiều lần chạy đã phát hiện ra một bảng ghi điểm 1726, nhưng nó luôn luôn là cùng một bảng được tìm thấy (bỏ qua các phép quay và phản xạ).
Như một thử nghiệm khác, tôi đã chạy chương trình của mình bằng cách sử dụng YAWL làm từ điển và nó đã tìm thấy bảng 4527 điểm sau khoảng một chục lần chạy. Vì điều này, tôi giả thuyết rằng chương trình của tôi đã ở giới hạn trên của không gian tìm kiếm, và do đó, việc viết lại mà tôi đang lên kế hoạch sẽ giới thiệu thêm độ phức tạp để thu được rất ít.
Dưới đây là danh sách năm bảng điểm cao nhất mà chương trình của tôi đã tìm thấy bằng cách sử dụng english.0
danh sách từ:
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
Tôi tin rằng "bảng grep" năm 1772 (như tôi đã gọi nó), với 531 từ, là bảng có điểm cao nhất có thể với danh sách từ này. Hơn 50% thời lượng hai phút của chương trình của tôi kết thúc với bảng này. Tôi cũng đã để chương trình của mình chạy qua đêm mà không tìm thấy gì tốt hơn. Vì vậy, nếu có một bảng điểm cao hơn, nó có thể sẽ phải có một số khía cạnh đánh bại kỹ thuật tìm kiếm của chương trình. Ví dụ, một bảng trong đó mọi thay đổi nhỏ có thể xảy ra đối với bố cục gây ra sự sụt giảm lớn trong tổng số điểm, chẳng hạn, có thể không bao giờ được phát hiện bởi chương trình của tôi. Linh cảm của tôi là một bảng như vậy rất khó tồn tại.
#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;
}
Ghi chú cho phiên bản 2 (ngày 9 tháng 6)
Đây là một cách để sử dụng phiên bản ban đầu của mã của tôi làm điểm xuất phát. Các thay đổi của phiên bản này bao gồm ít hơn 100 dòng, nhưng tăng gấp ba điểm số trò chơi trung bình.
Trong phiên bản này, chương trình duy trì một "nhóm" các ứng cử viên, bao gồm N bảng điểm cao nhất mà chương trình đã tạo ra cho đến nay. Mỗi khi một bảng mới được tạo, nó sẽ được thêm vào nhóm và bảng có điểm thấp nhất trong nhóm sẽ bị xóa (rất có thể là bảng vừa được thêm vào, nếu điểm của nó thấp hơn những gì đã có). Nhóm ban đầu được lấp đầy với các bảng được tạo ngẫu nhiên, sau đó nó giữ kích thước không đổi trong suốt quá trình chạy chương trình.
Vòng lặp chính của chương trình bao gồm việc chọn một bảng ngẫu nhiên từ nhóm và thay đổi nó, xác định điểm số của bảng mới này và sau đó đưa nó vào nhóm (nếu nó đủ điểm). Theo cách này, chương trình liên tục cải tiến các bảng điểm cao. Hoạt động chính là thực hiện các cải tiến từng bước, tăng dần, nhưng kích thước của nhóm cũng cho phép chương trình tìm các cải tiến nhiều bước tạm thời làm cho điểm của bảng trở nên tồi tệ hơn trước khi có thể làm cho nó tốt hơn.
Thông thường, chương trình này tìm thấy một mức tối đa cục bộ khá nhanh, sau đó có lẽ là bất kỳ mức tối đa tốt hơn nào là quá xa để tìm thấy. Và một lần nữa, có một điểm nhỏ để chạy chương trình dài hơn 10 giây. Điều này có thể được cải thiện bằng cách ví dụ như chương trình phát hiện tình huống này và bắt đầu tìm kiếm mới với nhóm ứng viên mới. Tuy nhiên, điều này sẽ chỉ tăng một chút. Một kỹ thuật tìm kiếm heuristic thích hợp có thể sẽ là một con đường khám phá tốt hơn.
(Lưu ý bên lề: Tôi thấy rằng phiên bản này đã tạo ra khoảng 5k bảng / giây. Vì phiên bản đầu tiên thường làm 20k bảng / giây, ban đầu tôi lo ngại. Tuy nhiên, khi tôi tìm hiểu thêm rằng tôi đã dành thời gian để quản lý danh sách từ. Nói cách khác, đó hoàn toàn là do chương trình tìm thấy rất nhiều từ trên mỗi bảng. Về vấn đề này, tôi đã cân nhắc việc thay đổi mã để quản lý danh sách từ, nhưng cho rằng chương trình này chỉ sử dụng 10 trong số 120 giây được phân bổ, như vậy tối ưu hóa sẽ rất sớm.)
Ghi chú cho phiên bản 1 (ngày 2 tháng 6)
Đây là một giải pháp rất, rất đơn giản. Tất cả những gì nó tạo ra các bảng ngẫu nhiên, và sau 10 giây, nó xuất ra bảng có số điểm cao nhất. (Tôi mặc định là 10 giây vì thêm 110 giây được cho phép bởi đặc tả vấn đề thường không cải thiện giải pháp cuối cùng đủ để đáng chờ đợi.) Vì vậy, nó cực kỳ ngu ngốc. Tuy nhiên, nó có tất cả các cơ sở hạ tầng để tạo điểm khởi đầu tốt cho tìm kiếm thông minh hơn và nếu có ai muốn sử dụng nó trước thời hạn, tôi khuyến khích họ làm như vậy.
Chương trình bắt đầu bằng cách đọc từ điển thành một cấu trúc cây. (Biểu mẫu không hoàn toàn được tối ưu hóa như có thể, nhưng nó không đủ tốt cho các mục đích này.) Sau một số khởi tạo cơ bản khác, sau đó nó bắt đầu tạo bảng và ghi điểm. Chương trình kiểm tra khoảng 20k bảng mỗi giây trên máy của tôi và sau khoảng 200 nghìn bảng, phương pháp ngẫu nhiên bắt đầu cạn.
Vì chỉ có một bảng thực sự được đánh giá tại bất kỳ thời điểm nào, dữ liệu cho điểm được lưu trữ trong các biến toàn cục. Điều này cho phép tôi giảm thiểu lượng dữ liệu không đổi phải được truyền dưới dạng đối số cho các hàm đệ quy. (Tôi chắc chắn rằng điều này sẽ cung cấp cho một số người tổ ong và tôi xin lỗi.) Danh sách từ được lưu trữ dưới dạng cây tìm kiếm nhị phân. Mỗi từ được tìm thấy phải được tra cứu trong danh sách từ, để các từ trùng lặp không được tính hai lần. Tuy nhiên, danh sách từ chỉ cần thiết trong quá trình sơ tán, vì vậy nó bị loại bỏ sau khi tìm thấy điểm. Do đó, vào cuối chương trình, bảng được chọn phải được ghi lại một lần nữa, để có thể in ra danh sách từ.
Sự thật thú vị: Điểm trung bình cho một bảng Boggle được tạo ngẫu nhiên, như được ghi bởi english.0
, là 61,7 điểm.
4527
(1414
tổng số từ), được tìm thấy ở đây: ai.stanford.edu/~chuongdo/boggle/index.html