C, com média de 500+ 1500 1750 pontos
Este é um aprimoramento relativamente menor em relação à versão 2 (veja abaixo as notas nas versões anteriores). Existem duas partes. Primeiro: em vez de selecionar pranchas aleatoriamente da piscina, o programa agora repete todas as pranchas da piscina, usando cada uma delas antes de retornar ao topo da piscina e repetir. (Como o pool está sendo modificado enquanto essa iteração ocorre, ainda haverá placas escolhidas duas vezes seguidas, ou pior, mas isso não é uma preocupação séria.) A segunda alteração é que o programa agora rastreia quando o pool muda e se o programa demorar muito sem melhorar o conteúdo do pool, ele determina que a pesquisa "parou", esvazia o pool e inicia novamente com uma nova pesquisa. Ele continua fazendo isso até que os dois minutos terminem.
Inicialmente, pensei que estaria empregando algum tipo de pesquisa heurística para ir além da faixa de 1500 pontos. O comentário de @ mellamokb sobre um quadro de 4527 pontos me levou a supor que havia muito espaço para melhorias. No entanto, estamos usando uma lista de palavras relativamente pequena. O quadro de 4527 pontos estava usando o YAWL, que é a lista de palavras mais inclusiva do mercado - é ainda maior que a lista de palavras oficial do Scrabble dos EUA. Com isso em mente, examinei novamente as placas que meu programa havia encontrado e notei que parecia haver um conjunto limitado de placas acima de 1700. Por exemplo, eu tive várias execuções que descobriram uma placa com a pontuação de 1726, mas sempre foi exatamente a mesma placa que foi encontrada (ignorando rotações e reflexões).
Como outro teste, executei meu programa usando YAWL como dicionário e ele encontrou o quadro de 4527 pontos após cerca de uma dúzia de execuções. Diante disso, suponho que meu programa já esteja no limite superior do espaço de pesquisa e, portanto, a reescrita que eu estava planejando introduziria complexidade extra com muito pouco ganho.
Aqui está minha lista dos cinco quadros com maior pontuação que meu programa encontrou usando a lista de english.0
palavras:
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
Minha crença é que o "quadro grep" de 1772 (como passei a chamá-lo), com 531 palavras, é o quadro com a maior pontuação possível com esta lista de palavras. Mais de 50% das execuções de dois minutos do meu programa terminam com este quadro. Também deixei meu programa em execução durante a noite sem encontrar nada melhor. Portanto, se houver um quadro de pontuação mais alta, é provável que ele tenha algum aspecto que derrote a técnica de pesquisa do programa. Um quadro em que todas as pequenas alterações possíveis no layout causam uma enorme queda na pontuação total, por exemplo, nunca podem ser descobertas pelo meu programa. Meu palpite é que é improvável que exista tal placa.
#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;
}
Notas para a versão 2 (9 de junho)
Aqui está uma maneira de usar a versão inicial do meu código como um ponto de partida. As alterações nesta versão consistem em menos de 100 linhas, mas triplicaram a pontuação média do jogo.
Nesta versão, o programa mantém um "pool" de candidatos, composto pelos N quadros de maior pontuação que o programa gerou até o momento. Toda vez que um novo quadro é gerado, ele é adicionado ao pool e o quadro de menor pontuação do pool é removido (que pode muito bem ser o quadro que acabou de ser adicionado, se sua pontuação for menor do que o que já existe). O pool é preenchido inicialmente com painéis gerados aleatoriamente, após o qual mantém um tamanho constante durante toda a execução do programa.
O loop principal do programa consiste em selecionar um tabuleiro aleatório do pool e alterá-lo, determinar a pontuação desse novo tabuleiro e colocá-lo no pool (se tiver uma pontuação suficiente). Dessa maneira, o programa aprimora continuamente as placas de alta pontuação. A principal atividade é fazer melhorias incrementais passo a passo, mas o tamanho do pool também permite que o programa encontre aprimoramentos em várias etapas que pioram temporariamente a pontuação de um conselho antes que ele possa melhorar.
Normalmente, este programa encontra um bom máximo local rapidamente, após o que, presumivelmente, qualquer máximo melhor está muito distante para ser encontrado. E, mais uma vez, não faz sentido executar o programa por mais de 10 segundos. Isso pode ser melhorado, por exemplo, com o programa detectando essa situação e iniciando uma nova pesquisa com um novo pool de candidatos. No entanto, isso representaria apenas um aumento marginal. Uma técnica de pesquisa heurística adequada provavelmente seria uma avenida melhor de exploração.
(Nota: vi que esta versão estava gerando cerca de 5k placas / s. Como a primeira versão normalmente produzia 20k placas / s, fiquei inicialmente preocupado. Após a criação de perfis, no entanto, descobri que o tempo extra era gasto gerenciando a lista de palavras. Em outras palavras, isso se deveu inteiramente ao fato de o programa ter encontrado muito mais palavras por painel.Em vista disso, considerei mudar o código para gerenciar a lista de palavras, mas, como esse programa está usando apenas 10 dos 120 segundos alocados, uma otimização seria muito prematura.)
Notas para a versão 1 (2 de junho)
Esta é uma solução muito, muito simples. Tudo isso gera placas aleatórias e, depois de 10 segundos, produz aquela com a maior pontuação. (O padrão foi 10 segundos porque os 110 segundos extras permitidos pela especificação do problema geralmente não melhoram a solução final encontrada o suficiente para valer a pena esperar.) Portanto, é extremamente idiota. No entanto, ele possui toda a infraestrutura para constituir um bom ponto de partida para uma pesquisa mais inteligente e, se alguém desejar utilizá-la antes do prazo final, incentivo-os a fazê-lo.
O programa começa lendo o dicionário em uma estrutura em árvore. (O formulário não é tão otimizado quanto poderia ser, mas é mais do que suficiente para esses fins.) Após algumas outras inicializações básicas, ele começa a gerar quadros e a classificá-los. O programa examina cerca de 20 mil placas por segundo na minha máquina e, após cerca de 200 mil placas, a abordagem aleatória começa a ficar seca.
Como apenas uma placa está realmente sendo avaliada a qualquer momento, os dados da pontuação são armazenados em variáveis globais. Isso me permite minimizar a quantidade de dados constantes que precisam ser passados como argumentos para as funções recursivas. (Tenho certeza de que isso dará algumas pessoas para as pessoas e peço desculpas.) A lista de palavras é armazenada como uma árvore de pesquisa binária. Todas as palavras encontradas devem ser pesquisadas na lista de palavras, para que palavras duplicadas não sejam contadas duas vezes. A lista de palavras é necessária apenas durante o processo de avaliação, no entanto, é descartada depois que a pontuação é encontrada. Assim, no final do programa, o painel escolhido deve ser pontuado novamente, para que a lista de palavras possa ser impressa.
Curiosidade: a pontuação média de um painel Boggle gerado aleatoriamente, conforme pontuado por english.0
, é de 61,7 pontos.
4527
(1414
total de palavras), encontrada aqui: ai.stanford.edu/~chuongdo/boggle/index.html