C, rata-rata 500+ 1500 1750 poin
Ini adalah peningkatan yang relatif kecil dibandingkan versi 2 (lihat di bawah untuk catatan tentang versi sebelumnya). Ada dua bagian. Pertama: Alih-alih memilih papan secara acak dari kolam, program sekarang beralih ke setiap papan di kolam, menggunakan masing-masing secara bergiliran sebelum kembali ke atas kolam dan mengulangi. (Karena pool sedang dimodifikasi saat iterasi ini terjadi, masih akan ada papan yang dipilih dua kali berturut-turut, atau lebih buruk, tapi ini bukan masalah serius.) Perubahan kedua adalah bahwa program sekarang melacak ketika pool berubah , dan jika program berjalan terlalu lama tanpa memperbaiki konten kumpulan, itu menentukan bahwa pencarian telah "terhenti", mengosongkan kumpulan, dan memulai kembali dengan pencarian baru. Terus melakukan ini sampai dua menit habis.
Awalnya saya berpikir bahwa saya akan menggunakan semacam pencarian heuristik untuk melampaui kisaran 1500 poin. Komentar @ mellamokb tentang board 4527-point membuat saya berasumsi bahwa ada banyak ruang untuk perbaikan. Namun, kami menggunakan daftar kata yang relatif kecil. Papan 4527-point dinilai menggunakan YAWL, yang merupakan daftar kata yang paling inklusif di luar sana - bahkan lebih besar dari daftar kata resmi Scrabble AS. Dengan mengingat hal ini, saya memeriksa kembali papan-papan yang telah ditemukan oleh program saya dan memerhatikan bahwa ada set papan terbatas di atas sekitar 1700 atau lebih. Jadi misalnya, saya memiliki beberapa kali menjalankan yang telah menemukan papan skor 1726, tetapi selalu papan yang sama persis yang ditemukan (mengabaikan rotasi dan refleksi).
Sebagai tes lain, saya menjalankan program saya menggunakan YAWL sebagai kamus, dan ia menemukan papan 4527-point setelah sekitar selusin berjalan. Mengingat ini, saya berhipotesis bahwa program saya sudah berada di batas atas ruang pencarian, dan karena itu penulisan ulang yang saya rencanakan akan memperkenalkan kompleksitas ekstra untuk keuntungan yang sangat sedikit.
Berikut adalah daftar lima papan skor tertinggi yang ditemukan program saya menggunakan english.0
daftar kata:
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
Keyakinan saya adalah bahwa "papan grep" 1772 (sebagaimana saya sering menyebutnya), dengan 531 kata, adalah papan skor tertinggi yang mungkin dengan daftar kata ini. Lebih dari 50% dari dua menit program saya berakhir dengan board ini. Saya juga membiarkan program saya berjalan semalaman tanpa menemukan sesuatu yang lebih baik. Jadi jika ada papan skor yang lebih tinggi, kemungkinan harus memiliki beberapa aspek yang mengalahkan teknik pencarian program. Papan di mana setiap perubahan kecil yang mungkin pada tata letak menyebabkan penurunan besar dalam skor total, misalnya, mungkin tidak pernah ditemukan oleh program saya. Firasat saya adalah bahwa papan seperti itu sangat tidak mungkin ada.
#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;
}
Catatan untuk versi 2 (9 Juni)
Inilah salah satu cara untuk menggunakan versi awal kode saya sebagai titik awal. Perubahan pada versi ini terdiri dari kurang dari 100 baris, tetapi tiga kali lipat skor game rata-rata.
Dalam versi ini, program memiliki "kumpulan" kandidat, yang terdiri dari N papan skor tertinggi yang telah dihasilkan oleh program sejauh ini. Setiap kali papan baru dibuat, ditambahkan ke kolam dan papan skor terendah di kolam itu dihapus (yang mungkin merupakan papan yang baru saja ditambahkan, jika nilainya lebih rendah dari yang sudah ada). Kelompok ini awalnya diisi dengan papan yang dibuat secara acak, setelah itu ukurannya konstan sepanjang program dijalankan.
Lingkaran utama program terdiri dari memilih papan acak dari kumpulan dan mengubahnya, menentukan skor papan baru ini dan kemudian memasukkannya ke dalam kumpulan (jika skornya cukup baik). Dengan cara ini, program ini terus menyempurnakan papan skor tinggi. Kegiatan utama adalah membuat peningkatan bertahap, bertahap, tetapi ukuran kelompok ini juga memungkinkan program menemukan peningkatan multi-langkah yang sementara membuat skor dewan lebih buruk sebelum dapat membuatnya lebih baik.
Biasanya program ini menemukan maksimum lokal yang baik agak cepat, setelah itu mungkin ada maksimum yang lebih baik terlalu jauh untuk ditemukan. Jadi sekali lagi ada gunanya menjalankan program lebih dari 10 detik. Ini dapat ditingkatkan dengan misalnya membuat program mendeteksi situasi ini dan memulai pencarian baru dengan kumpulan kandidat baru. Namun, ini hanya akan menambah sedikit kenaikan. Teknik pencarian heuristik yang tepat kemungkinan akan menjadi jalan eksplorasi yang lebih baik.
(Catatan: Saya melihat bahwa versi ini menghasilkan sekitar 5k papan / detik. Karena versi pertama biasanya 20k papan / detik, saya awalnya khawatir. Namun setelah membuat profil, saya menemukan bahwa waktu ekstra dihabiskan untuk mengelola daftar kata. Dengan kata lain, itu sepenuhnya karena program menemukan lebih banyak kata per papan. Sehubungan dengan ini, saya mempertimbangkan untuk mengubah kode untuk mengelola daftar kata, tetapi mengingat bahwa program ini hanya menggunakan 10 dari 120 jatah yang dialokasikan, seperti optimasi akan sangat prematur.)
Catatan untuk versi 1 (2 Juni)
Ini adalah solusi yang sangat, sangat sederhana. Semua itu menghasilkan papan acak, dan kemudian setelah 10 detik output yang satu dengan skor tertinggi. (Saya default ke 10 detik karena tambahan 110 detik yang diizinkan oleh spesifikasi masalah biasanya tidak meningkatkan solusi akhir yang cukup layak untuk ditunggu.) Jadi itu sangat bodoh. Namun, ia memiliki semua infrastruktur untuk membuat titik awal yang baik untuk pencarian yang lebih cerdas, dan jika ada yang ingin memanfaatkannya sebelum batas waktu, saya mendorong mereka untuk melakukannya.
Program dimulai dengan membaca kamus ke dalam struktur pohon. (Bentuknya tidak seoptimal mungkin, tetapi lebih dari cukup untuk keperluan ini.) Setelah beberapa inisialisasi dasar lainnya, ia kemudian mulai membuat papan dan mencetaknya. Program memeriksa sekitar 20rb papan per detik pada mesin saya, dan setelah sekitar 200rb papan pendekatan acak mulai berjalan kering.
Karena hanya satu papan yang benar-benar dievaluasi pada waktu tertentu, data penilaian disimpan dalam variabel global. Ini memungkinkan saya untuk meminimalkan jumlah data konstan yang harus dilewati sebagai argumen untuk fungsi rekursif. (Saya yakin ini akan memberi beberapa orang gatal-gatal, dan kepada mereka saya minta maaf.) Daftar kata disimpan sebagai pohon pencarian biner. Setiap kata yang ditemukan harus dicari di daftar kata, sehingga kata-kata duplikat tidak dihitung dua kali. Namun, daftar kata hanya diperlukan selama proses evaulasi, sehingga dihapus setelah skor ditemukan. Dengan demikian, pada akhir program, papan yang dipilih harus dicetak lagi, sehingga daftar kata dapat dicetak.
Fakta menyenangkan: Skor rata-rata untuk papan Boggle yang dibuat secara acak, seperti yang dicetak oleh english.0
, adalah 61,7 poin.
4527
(1414
kata-kata total), ditemukan di sini: ai.stanford.edu/ ~ chuongdo