La mejor tabla de Boggle de puntuación

16

Estaba interesado en ver las respuestas a esta pregunta (ahora desaparecida) , pero nunca se ha corregido / mejorado.

Dado un conjunto de dados Boggle de 6 lados (configuración robada de esta pregunta ), determine en dos minutos de tiempo de procesamiento qué configuración del tablero permitirá el puntaje más alto posible. (es decir, ¿qué dado en qué ubicación con qué lado hacia arriba permite el mayor grupo de palabras de puntuación?)


OBJETIVO

  • Su código no debe ejecutarse por más de 2 minutos (120 segundos). En ese momento, debería dejar de ejecutarse automáticamente e imprimir los resultados.

  • El puntaje final del desafío será el puntaje promedio de Boggle de 5 carreras del programa.

    • En caso de empate, el ganador será el algoritmo que encuentre más palabras.
    • En caso de que aún haya un empate, el ganador será el algoritmo que encuentre palabras más largas (8+) .

NORMAS / RESTRICCIONES

  • Este es un desafío de código; la longitud del código es irrelevante.

  • Consulte este enlace para obtener una lista de palabras (use la ISPELL "english.0"lista: a la lista SCOWL le faltan algunas palabras bastante comunes).

    • Se puede hacer referencia / importar / leer este listado en su código de la forma que desee.
    • Solo ^([a-pr-z]|qu){3,16}$se contarán las palabras que coincidan con la expresión regular . (Solo letras minúsculas, 3-16 caracteres, qu deben usarse como una unidad).
  • Las palabras se forman uniendo letras adyacentes (horizontal, vertical y diagonal) para deletrear las palabras en el orden correcto, sin usar un solo dado más de una vez en una sola palabra.

    • Las palabras deben tener 3 letras o más; palabras más cortas no ganarán puntos.
    • Las letras duplicadas son aceptables, simplemente no dados.
    • No se permiten palabras que abarquen los bordes / se crucen de un lado del tablero al otro.
  • El puntaje final de Boggle ( no desafío ) es el total de los valores de puntos para todas las palabras que se encuentran.

    • El valor del punto asignado para cada palabra se basa en la longitud de la palabra. (vea abajo)
    • Las reglas normales de Boggle deducirían / ​​descontarían las palabras encontradas por otro jugador. Suponga que aquí no hay otros jugadores involucrados y que todas las palabras encontradas cuentan para la puntuación total.
    • Sin embargo, las palabras que se encuentran más de una vez en la misma cuadrícula solo deben contarse una vez.
  • Su función / programa debe ENCONTRAR la disposición óptima; simplemente codificar una lista predeterminada no funcionará.

  • Su salida debe ser una cuadrícula de 4x4 de su tablero de juego ideal, una lista de todas las palabras encontradas para ese tablero y la puntuación de Boggle para que coincida con esas palabras.


CONFIGURACIÓN DE MUERTE

A  A  E  E  G  N
E  L  R  T  T  Y
A  O  O  T  T  W
A  B  B  J  O  O
E  H  R  T  V  W
C  I  M  O  T  U
D  I  S  T  T  Y
E  I  O  S  S  T
D  E  L  R  V  Y
A  C  H  O  P  S
H  I  M  N  Qu U
E  E  I  N  S  U
E  E  G  H  N  W
A  F  F  K  P  S
H  L  N  N  R  Z
D  E  I  L  R  X

TABLA DE PUNTUACIÓN DE BOGGLE ESTÁNDAR

Word length => Points
<= 2 - 0 pts
   3 - 1  
   4 - 1  
   5 - 2  
   6 - 3  
   7 - 5
>= 8 - 11 pts
*Words using the "Qu" die will count the full 2 letters for their word, not just the 1 die.

EJEMPLO DE SALIDA

A  L  O  J  
V  U  T  S  
L  C  H  E  
G  K  R  X

CUT
THE
LUCK
HEX
....

140 points

Si necesita más aclaraciones, ¡pregunte!

Gaffi
fuente
2
Preferiría tener un diccionario para estandarizar el objetivo. Tenga en cuenta también que esta no es una idea nueva, ya que una simple búsqueda en Google revelará :) El puntaje más alto que he visto es 4527( 1414palabras totales), que se encuentra aquí: ai.stanford.edu/~chuongdo/boggle/index.html
mellamokb
44
¿Se requiere que el programa termine este siglo?
Peter Taylor
1
@GlitchMr En inglés, Q normalmente solo se usa con U. Boggle explica esto poniendo las dos letras en el mismo dado como una unidad.
Gaffi
1
La especificación de la lista de palabras no está clara. ¿Estás contando solo las palabras en inglés.0 en minúsculas? (Las reglas estándar del juego de palabras excluyen abreviaturas / initialisms y nombres propios).
Peter Taylor
1
Estaba pensando en expresiones regulares ^([a-pr-z]|qu){3,16}$(que excluiría incorrectamente las palabras de 3 letras con qu, pero no hay ninguna).
Peter Taylor

Respuestas:

9

C, promediando 500+ 1500 1750 puntos

Esta es una mejora relativamente menor con respecto a la versión 2 (ver abajo las notas sobre versiones anteriores). Hay dos partes Primero: en lugar de seleccionar tableros al azar del grupo, el programa ahora itera sobre cada tablero en el grupo, usando cada uno por turno antes de regresar a la parte superior del grupo y repetir. (Dado que el grupo se modifica mientras se produce esta iteración, todavía habrá paneles que se eligen dos veces seguidas, o peor, pero esto no es una preocupación seria). El segundo cambio es que el programa ahora rastrea cuando el grupo cambia , y si el programa dura demasiado tiempo sin mejorar el contenido del grupo, determina que la búsqueda se ha "estancado", vacía el grupo y comienza de nuevo con una nueva búsqueda. Continúa haciendo esto hasta que hayan transcurrido los dos minutos.

Al principio pensé que estaría empleando algún tipo de búsqueda heurística para superar el rango de 1500 puntos. El comentario de @ mellamokb sobre un tablero de 4527 puntos me llevó a suponer que había mucho margen de mejora. Sin embargo, estamos usando una lista de palabras relativamente pequeña. El tablero de 4527 puntos estaba anotando usando YAWL, que es la lista de palabras más inclusiva que existe, es incluso más grande que la lista de palabras oficial de Scrabble de EE. UU. Con esto en mente, volví a examinar los tableros que mi programa había encontrado y noté que parecía haber un conjunto limitado de tableros por encima de 1700 más o menos. Entonces, por ejemplo, tuve varias carreras que descubrieron un tablero con 1726, pero siempre fue el mismo tablero que se encontró (ignorando rotaciones y reflexiones).

Como otra prueba, ejecuté mi programa usando YAWL como diccionario, y encontré el tablero de 4527 puntos después de aproximadamente una docena de carreras. Dado esto, estoy planteando la hipótesis de que mi programa ya está en el límite superior del espacio de búsqueda y, por lo tanto, la reescritura que estaba planeando introduciría una complejidad adicional para muy poca ganancia.

Aquí está mi lista de los cinco tableros de mayor puntaje que mi programa ha encontrado usando la english.0lista de palabras:

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

Creo que el "tablero grep" de 1772 (como lo he llamado), con 531 palabras, es el tablero con la puntuación más alta posible con esta lista de palabras. Más del 50% de las ejecuciones de dos minutos de mi programa terminan con esta placa. También dejé mi programa ejecutándose durante la noche sin encontrar nada mejor. Entonces, si hay un tablero con una puntuación más alta, es probable que tenga algún aspecto que derrote la técnica de búsqueda del programa. Un tablero en el que cada pequeño cambio posible en el diseño causa una gran caída en el puntaje total, por ejemplo, nunca podría ser descubierto por mi programa. Mi presentimiento es que es muy poco probable que exista tal tablero.

#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 la versión 2 (9 de junio)

Aquí hay una forma de usar la versión inicial de mi código como punto de partida. Los cambios a esta versión consisten en menos de 100 líneas, pero triplicaron el puntaje promedio del juego.

En esta versión, el programa mantiene un "grupo" de candidatos, que consta de los N tableros con la puntuación más alta que el programa ha generado hasta ahora. Cada vez que se genera un nuevo tablero, se agrega al grupo y se elimina el tablero con el puntaje más bajo en el grupo (que muy bien podría ser el tablero que se acaba de agregar, si su puntaje es más bajo que el que ya está allí). El grupo se llena inicialmente con paneles generados aleatoriamente, después de lo cual mantiene un tamaño constante durante la ejecución del programa.

El ciclo principal del programa consiste en seleccionar un tablero aleatorio del grupo y modificarlo, determinar el puntaje de este nuevo tablero y luego colocarlo en el grupo (si obtiene un puntaje suficiente). De esta manera, el programa refina continuamente tableros de alto puntaje. La actividad principal es realizar mejoras graduales e incrementales, pero el tamaño de la agrupación también permite que el programa encuentre mejoras de varios pasos que empeoran temporalmente el puntaje de un tablero antes de que pueda mejorarlo.

Por lo general, este programa encuentra un buen máximo local con bastante rapidez, después de lo cual, presumiblemente, cualquier máximo mejor es demasiado distante para ser encontrado. Y así, una vez más, no tiene mucho sentido ejecutar el programa durante más de 10 segundos. Esto podría mejorarse, por ejemplo, haciendo que el programa detecte esta situación y comience una nueva búsqueda con un nuevo grupo de candidatos. Sin embargo, esto generaría solo un aumento marginal. Una técnica de búsqueda heurística adecuada probablemente sería una mejor vía de exploración.

(Nota al margen: vi que esta versión estaba generando alrededor de 5k tableros / seg. Como la primera versión típicamente producía 20k tableros / seg, al principio me preocupé. Sin embargo, al hacer un perfil, descubrí que el tiempo adicional se dedicó a administrar la lista de palabras. En otras palabras, se debió por completo al programa de encontrar muchas más palabras por placa. A la luz de esto, consideré cambiar el código para administrar la lista de palabras, pero dado que este programa solo usa 10 de sus 120 segundos asignados, tal una optimización sería muy prematura).

Notas para la versión 1 (2 de junio)

Esta es una solución muy, muy simple. Todo lo que hace es generar tableros aleatorios, y luego de 10 segundos genera el que tiene la puntuación más alta. (El valor predeterminado fue de 10 segundos porque los 110 segundos adicionales permitidos por la especificación del problema generalmente no mejoran la solución final encontrada lo suficiente como para que valga la pena esperar). Por lo tanto, es extremadamente tonto. Sin embargo, tiene toda la infraestructura para hacer un buen punto de partida para una búsqueda más inteligente, y si alguien desea utilizarla antes de la fecha límite, les animo a que lo hagan.

El programa comienza leyendo el diccionario en una estructura de árbol. (El formulario no está tan optimizado como podría estarlo, pero es más que suficiente para estos fines). Después de alguna otra inicialización básica, comienza a generar tablas y puntuarlas. El programa examina aproximadamente 20k tableros por segundo en mi máquina, y después de aproximadamente 200k tableros, el enfoque aleatorio comienza a funcionar en seco.

Como solo se está evaluando un tablero en un momento dado, los datos de puntuación se almacenan en variables globales. Esto me permite minimizar la cantidad de datos constantes que deben pasarse como argumentos a las funciones recursivas. (Estoy seguro de que esto les dará colmenas a algunas personas, y les pido disculpas). La lista de palabras se almacena como un árbol de búsqueda binario. Cada palabra encontrada debe buscarse en la lista de palabras, para que las palabras duplicadas no se cuenten dos veces. Sin embargo, la lista de palabras solo es necesaria durante el proceso de evacuación, por lo que se descarta después de encontrar el puntaje. Por lo tanto, al final del programa, la tabla elegida debe puntuarse nuevamente para que la lista de palabras se pueda imprimir.

Dato curioso: el puntaje promedio para un tablero Boggle generado aleatoriamente, según la puntuación english.0, es 61.7 puntos.

caja de pan
fuente
Obviamente, necesito mejorar mi propia eficiencia. :-)
Gaffi
Mi enfoque genético obtiene alrededor de 700-800 puntos generando alrededor de 200k tablas, por lo que claramente está haciendo algo mucho mejor que yo en la forma en que produce la próxima generación.
Peter Taylor
Mi propia estructura de árbol, que hasta ahora se ha implementado solo para la lista de palabras maestra, aunque funciona y me permite validar tableros, bloquea la memoria de mi sistema (se retrasa activamente hasta el punto de que toma un tiempo considerable forzar el proceso terminar antes de tiempo). Seguramente es mi culpa, ¡pero estoy trabajando en ello! Editar: ya solucionado! ;-)
Gaffi
@PeterTaylor Pensé en probar un algoritmo genético, pero no pude pensar en un mecanismo plausible para combinar dos tableros. ¿Cómo lo haces? ¿Estás seleccionando al padre al azar para cada ranura en el tablero?
breadbox
Divido el estado del tablero en la permutación de dados y las caras que se muestran en los dados. Para el crossover de permutación utilizo el "crossover de orden 1" de cs.colostate.edu/~genitor/1995/permutations.pdf y para el crossover facial hago lo obvio. Pero tengo una idea para un enfoque totalmente diferente que necesito encontrar el tiempo para implementar.
Peter Taylor
3

VBA (promedio que actualmente oscila entre 80 y 110 puntos, sin terminar)

Aquí está mi proceso de trabajo, pero está lejos de ser el mejor posible; mi mejor puntaje absoluto encontrado en cualquier tablero después de muchas pruebas es de alrededor de 120. Todavía debe haber una mejor limpieza general y estoy seguro de que se pueden obtener más eficiencias en varios lugares.

  • 2012.05.09:
    • Publicación original
  • 2012.05.10 - 2012.05.18:
    • Se mejoró el algoritmo de puntuación.
    • Se mejoró la lógica de pathfinding
  • 2012.06.07 - 2012.06.12 :
    • Se redujo el límite de palabras a 6 de 8. Permite más tableros con palabras más pequeñas. Parece haber hecho una ligera mejora en la puntuación promedio. (10 a 15 tableros revisados ​​por carrera vs. 1 a 2)
    • Siguiendo la sugerencia de breadbox, he creado una estructura de árbol para albergar la lista de palabras. Esto acelera la comprobación de back-end de las palabras en un tablero de manera significativa.
    • Jugué cambiando el tamaño máximo de palabra (velocidad vs. puntaje) y aún no he decidido si 5 o 6 es una mejor opción para mí. 6 da como resultado 100-120 tableros totales verificados, mientras que 5 da como resultado 500-1000 (los cuales todavía están muy por debajo de los otros ejemplos proporcionados hasta ahora).
    • Problema : después de muchas ejecuciones sucesivas, el proceso comienza a ralentizarse, por lo que aún queda algo de memoria para administrar.

Esto probablemente parezca horrible para algunos de ustedes, pero como dije, WIP. ¡Estoy muy abierto a la crítica constructiva ! Perdón por el cuerpo muy largo ...


Módulo de clase de dados :

Option Explicit

Private Sides() As String

Sub NewDie(NewLetters As String)
    Sides = Split(NewLetters, ",")
End Sub

Property Get Side(i As Integer)
    Side = Sides(i)
End Property

Módulo de clase de árbol :

Option Explicit

Private zzroot As TreeNode


Sub AddtoTree(ByVal TreeWord As Variant)
Dim i As Integer
Dim TempNode As TreeNode

    Set TempNode = TraverseTree(TreeWord, zzroot)
    SetNode TreeWord, TempNode

End Sub

Private Function SetNode(ByVal Value As Variant, parent As TreeNode) As TreeNode
Dim ValChar As String
    If Len(Value) > 0 Then
        ValChar = Left(Value, 1)
        Select Case Asc(ValChar) - 96
            Case 1:
                Set parent.Node01 = AddNode(ValChar, parent.Node01)
                Set SetNode = parent.Node01
            Case 2:
                Set parent.Node02 = AddNode(ValChar, parent.Node02)
                Set SetNode = parent.Node02
            ' ... - Reduced to limit size of answer.
            Case 26:
                Set parent.Node26 = AddNode(ValChar, parent.Node26)
                Set SetNode = parent.Node26
            Case Else:
                Set SetNode = Nothing
        End Select

        Set SetNode = SetNode(Right(Value, Len(Value) - 1), SetNode)
    Else
        Set parent.Node27 = AddNode(True, parent.Node27)
        Set SetNode = parent.Node27
    End If
End Function

Function AddNode(ByVal Value As Variant, NewNode As TreeNode) As TreeNode
    If NewNode Is Nothing Then
        Set AddNode = New TreeNode
        AddNode.Value = Value
    Else
        Set AddNode = NewNode
    End If
End Function
Function TraverseTree(TreeWord As Variant, parent As TreeNode) As TreeNode
Dim Node As TreeNode
Dim ValChar As String
    If Len(TreeWord) > 0 Then
        ValChar = Left(TreeWord, 1)

        Select Case Asc(ValChar) - 96
            Case 1:
                Set Node = parent.Node01
            Case 2:
                Set Node = parent.Node02
            ' ... - Reduced to limit size of answer.
            Case 26:
                Set Node = parent.Node26
            Case Else:
                Set Node = Nothing
        End Select

        If Not Node Is Nothing Then
            Set TraverseTree = TraverseTree(Right(TreeWord, Len(TreeWord) - 1), Node)
            If Not TraverseTree Is Nothing Then
                Set TraverseTree = parent
            End If
        Else
            Set TraverseTree = parent
        End If
    Else
        If parent.Node27.Value Then
            Set TraverseTree = parent
        Else
            Set TraverseTree = Nothing
        End If
    End If
End Function

Function WordScore(TreeWord As Variant, Step As Integer, Optional parent As TreeNode = Nothing) As Integer
Dim Node As TreeNode
Dim ValChar As String
    If parent Is Nothing Then Set parent = zzroot
    If Len(TreeWord) > 0 Then
        ValChar = Left(TreeWord, 1)

        Select Case Asc(ValChar) - 96
            Case 1:
                Set Node = parent.Node01
            Case 2:
                Set Node = parent.Node02
            ' ... - Reduced to limit size of answer.
            Case 26:
                Set Node = parent.Node26
            Case Else:
                Set Node = Nothing
        End Select

        If Not Node Is Nothing Then
            WordScore = WordScore(Right(TreeWord, Len(TreeWord) - 1), Step + 1, Node)
        End If
    Else
        If parent.Node27 Is Nothing Then
            WordScore = 0
        Else
            WordScore = Step
        End If
    End If
End Function

Function ValidWord(TreeWord As Variant, Optional parent As TreeNode = Nothing) As Integer
Dim Node As TreeNode
Dim ValChar As String
    If parent Is Nothing Then Set parent = zzroot
    If Len(TreeWord) > 0 Then
        ValChar = Left(TreeWord, 1)

        Select Case Asc(ValChar) - 96
            Case 1:
                Set Node = parent.Node01
            Case 2:
                Set Node = parent.Node02
            ' ... - Reduced to limit size of answer.
            Case 26:
                Set Node = parent.Node26
            Case Else:
                Set Node = Nothing
        End Select

        If Not Node Is Nothing Then
            ValidWord = ValidWord(Right(TreeWord, Len(TreeWord) - 1), Node)
        Else
            ValidWord = False
        End If
    Else
        If parent.Node27 Is Nothing Then
            ValidWord = False
        Else
            ValidWord = True
        End If
    End If
End Function

Private Sub Class_Initialize()
    Set zzroot = New TreeNode
End Sub

Private Sub Class_Terminate()
    Set zzroot = Nothing
End Sub

Módulo de clase TreeNode :

Option Explicit

Public Value As Variant
Public Node01 As TreeNode
Public Node02 As TreeNode
' ... - Reduced to limit size of answer.
Public Node26 As TreeNode
Public Node27 As TreeNode

Módulo principal :

Option Explicit

Const conAllSides As String = ";a,a,e,e,g,n;e,l,r,t,t,y;a,o,o,t,t,w;a,b,b,j,o,o;e,h,r,t,v,w;c,i,m,o,t,u;d,i,s,t,t,y;e,i,o,s,s,t;d,e,l,r,v,y;a,c,h,o,p,s;h,i,m,n,qu,u;e,e,i,n,s,u;e,e,g,h,n,w;a,f,f,k,p,s;h,l,n,n,r,z;d,e,i,l,r,x;"
Dim strBoard As String, strBoardTemp As String, strWords As String, strWordsTemp As String
Dim CheckWordSub As String
Dim iScore As Integer, iScoreTemp As Integer
Dim Board(1 To 4, 1 To 4) As Integer
Dim AllDice(1 To 16) As Dice
Dim AllWordsTree As Tree
Dim AllWords As Scripting.Dictionary
Dim CurWords As Scripting.Dictionary
Dim FullWords As Scripting.Dictionary
Dim JunkWords As Scripting.Dictionary
Dim WordPrefixes As Scripting.Dictionary
Dim StartTime As Date, StopTime As Date
Const MAX_LENGTH As Integer = 5
Dim Points(3 To 8) As Integer

Sub Boggle()
Dim DiceSetup() As String
Dim i As Integer, j As Integer, k As Integer

    StartTime = Now()

    strBoard = vbNullString
    strWords = vbNullString
    iScore = 0

    ReadWordsFileTree

    DiceSetup = Split(conAllSides, ";")

    For i = 1 To 16
        Set AllDice(i) = New Dice
        AllDice(i).NewDie "," & DiceSetup(i)
    Next i

    Do While WithinTimeLimit

        Shuffle

        strBoardTemp = vbNullString
        strWordsTemp = vbNullString
        iScoreTemp = 0

        FindWords

        If iScoreTemp > iScore Or iScore = 0 Then
            iScore = iScoreTemp
            k = 1
            For i = 1 To 4
                For j = 1 To 4
                    strBoardTemp = strBoardTemp & AllDice(k).Side(Board(j, i)) & "  "
                    k = k + 1
                Next j
                strBoardTemp = strBoardTemp & vbNewLine
            Next i
            strBoard = strBoardTemp
            strWords = strWordsTemp

        End If

    Loop

    Debug.Print strBoard
    Debug.Print strWords
    Debug.Print iScore & " points"

    Set AllWordsTree = Nothing
    Set AllWords = Nothing
    Set CurWords = Nothing
    Set FullWords = Nothing
    Set JunkWords = Nothing
    Set WordPrefixes = Nothing

End Sub

Sub ShuffleBoard()
Dim i As Integer

    For i = 1 To 16
        If Not WithinTimeLimit Then Exit Sub
        Board(Int((i - 1) / 4) + 1, 4 - (i Mod 4)) = Int(6 * Rnd() + 1)
    Next i

End Sub

Sub Shuffle()
Dim n As Long
Dim Temp As Variant
Dim j As Long

    Randomize
    ShuffleBoard
    For n = 1 To 16
        If Not WithinTimeLimit Then Exit Sub
        j = CLng(((16 - n) * Rnd) + n)
        If n <> j Then
            Set Temp = AllDice(n)
            Set AllDice(n) = AllDice(j)
            Set AllDice(j) = Temp
        End If
    Next n

    Set FullWords = New Scripting.Dictionary
    Set CurWords = New Scripting.Dictionary
    Set JunkWords = New Scripting.Dictionary

End Sub

Sub ReadWordsFileTree()
Dim FSO As New FileSystemObject
Dim FS
Dim strTemp As Variant
Dim iLength As Integer
Dim StartTime As Date

    StartTime = Now()
    Set AllWordsTree = New Tree
    Set FS = FSO.OpenTextFile("P:\Personal\english.txt")

    Points(3) = 1
    Points(4) = 1
    Points(5) = 2
    Points(6) = 3
    Points(7) = 5
    Points(8) = 11

    Do Until FS.AtEndOfStream
        strTemp = FS.ReadLine
        If strTemp = LCase(strTemp) Then
            iLength = Len(strTemp)
            iLength = IIf(iLength > 8, 8, iLength)
            If InStr(strTemp, "'") < 1 And iLength > 2 Then
                AllWordsTree.AddtoTree strTemp
            End If
        End If
    Loop
    FS.Close

End Sub

Function GetScoreTree() As Integer
Dim TempScore As Integer

    If Not WithinTimeLimit Then Exit Function

    GetScoreTree = 0

    TempScore = AllWordsTree.WordScore(CheckWordSub, 0)
    Select Case TempScore
        Case Is < 3:
            GetScoreTree = 0
        Case Is > 8:
            GetScoreTree = 11
        Case Else:
            GetScoreTree = Points(TempScore)
    End Select

End Function

Sub SubWords(CheckWord As String)
Dim CheckWordScore As Integer
Dim k As Integer, l As Integer

    For l = 0 To Len(CheckWord) - 3
        For k = 1 To Len(CheckWord) - l
            If Not WithinTimeLimit Then Exit Sub

            CheckWordSub = Mid(CheckWord, k, Len(CheckWord) - ((k + l) - 1))

            If Len(CheckWordSub) >= 3 And Not CurWords.Exists(CheckWordSub) Then
                CheckWordScore = GetScoreTree

                If CheckWordScore > 0 Then
                    CurWords.Add CheckWordSub, CheckWordSub
                    iScoreTemp = iScoreTemp + CheckWordScore
                    strWordsTemp = strWordsTemp & CheckWordSub & vbNewLine
                End If

                If Left(CheckWordSub, 1) = "q" Then
                    k = k + 1
                End If
            End If

        Next k
    Next l

End Sub

Sub FindWords()
Dim CheckWord As String
Dim strBoardLine(1 To 16) As String
Dim Used(1 To 16) As Boolean
Dim i As Integer, j As Integer, k As Integer, l As Integer, m As Integer, n As Integer
Dim StartSquare As Integer
Dim FullCheck As Variant

    n = 1
    For l = 1 To 4
        For m = 1 To 4
            If Not WithinTimeLimit Then Exit Sub
            strBoardLine(n) = AllDice(n).Side(Board(m, l))
            n = n + 1
        Next m
    Next l

    For i = 1 To 16
        For k = 1 To 16

            If Not WithinTimeLimit Then Exit Sub
            If k Mod 2 = 0 Then
                For j = 1 To 16
                    Used(j) = False
                Next j

                Used(i) = True
                MakeWords strBoardLine, Used, i, k / 2, strBoardLine(i)
            End If

        Next k
    Next i

    For Each FullCheck In FullWords.Items
        SubWords CStr(FullCheck)
    Next FullCheck

End Sub

Function MakeWords(BoardLine() As String, Used() As Boolean, _
    Start As Integer, _
    Direction As Integer, CurString As String) As String
Dim i As Integer, j As Integer, k As Integer, l As Integer

    j = 0

    Select Case Direction
        Case 1:
            k = Start - 5
        Case 2:
            k = Start - 4
        Case 3:
            k = Start - 3
        Case 4:
            k = Start - 1
        Case 5:
            k = Start + 1
        Case 6:
            k = Start + 3
        Case 7:
            k = Start + 4
        Case 8:
            k = Start + 5
    End Select

    If k >= 1 And k <= 16 Then
        If Not WithinTimeLimit Then Exit Function

        If Not Used(k) Then
            If ValidSquare(Start, k) Then
                If Not (JunkWords.Exists(CurString & BoardLine(k))) And Not FullWords.Exists(CurString & BoardLine(k)) Then
                    Used(k) = True
                    For l = 1 To MAX_LENGTH
                        If Not WithinTimeLimit Then Exit Function
                        MakeWords = CurString & BoardLine(k)
                        If Not (JunkWords.Exists(MakeWords)) Then
                            JunkWords.Add MakeWords, MakeWords
                        End If
                        If Len(MakeWords) = MAX_LENGTH And Not FullWords.Exists(MakeWords) Then
                            FullWords.Add MakeWords, MakeWords
                        ElseIf Len(MakeWords) < MAX_LENGTH Then
                            MakeWords BoardLine, Used, k, l, MakeWords
                        End If
                    Next l
                    Used(k) = False
                End If
            End If
        End If
    End If

    If Len(MakeWords) = MAX_LENGTH And Not FullWords.Exists(MakeWords) Then
        FullWords.Add MakeWords, MakeWords
        Debug.Print "FULL - " & MakeWords
    End If

End Function

Function ValidSquare(StartSquare As Integer, EndSquare As Integer) As Boolean
Dim sx As Integer, sy As Integer, ex As Integer, ey As Integer

    If Not WithinTimeLimit Then Exit Function

    sx = (StartSquare - 1) Mod 4 + 1
    ex = (EndSquare - 1) Mod 4 + 1

    sy = Int((StartSquare - 1) / 4 + 1)
    ey = Int((EndSquare - 1) / 4 + 1)

    ValidSquare = (sx - 1 <= ex And sx + 1 >= ex) And (sy - 1 <= ey And sy + 1 >= ey) And StartSquare <> EndSquare

End Function

Function WithinTimeLimit() As Boolean
    StopTime = Now()
    WithinTimeLimit = (Round(CDbl(((StopTime - StartTime) - Int(StopTime - StartTime)) * 86400), 0) < 120)
End Function
Gaffi
fuente
2
No he revisado el código, pero 50 puntos son ridículamente bajos. He jugado tableros generados al azar con puntajes de más de 1000 (usando SOWPODS; la lista de palabras suministrada puede ser menos extensa). ¡Es posible que desee verificar si hay un error de señal!
Peter Taylor
@ PeterTaylor Gracias por la sugerencia. Sé que la puntuación es demasiado baja, y sé que parte del problema radica en el hecho de que puedo ver las palabras obvias están perdiendo ...
Gaffi
@PeterTaylor Además, para que conste, estoy continuamente publicando mi progreso, en lugar de esperar mi producto final, ya que nadie más lo ha apuñalado todavía. Me gustaría mantener la pregunta algo viva hasta que eso suceda.
Gaffi
También debo tener en cuenta que esto no se ejecuta en la máquina más rápida que existe, por lo que tampoco ayuda.
Gaffi
1
@ Gaffi 10 segundos para calcular la puntuación? Eso es 9.999 segundos demasiado largo. Tienes que repensar tu código. Si se niega a convertir su lista de palabras en un árbol, al menos haga esto: haga una lista (tabla hash, lo que sea) de todos los prefijos de dos letras. Luego, cuando comience a seguir una ruta en el tablero, si las dos primeras letras no están en la lista, omita todo el subárbol de posibles rutas. Nuevamente, construir el árbol completo es lo mejor, pero la lista de prefijos de dos letras ayudará y es muy barato de hacer.
breadbox
2

Vista rápida del tamaño del espacio de búsqueda.

   16! => 20922789888000 Dice Permutations
(6^16) =>  2821109907456 Face Permutations
 59025489844657012604928000 Boggle Grids 

Reducir para excluir la repetición en cada dado.

              16! => 20922789888000 Dice Permutations
(4^4)*(5^6)*(6^5) => 31104000000 Unique Face Permutations
   650782456676352000000000 Boggle Grids 

@breadbox almacena el diccionario como una comprobación Hash Table O (1).

EDITAR

Mejor tablero (que he presenciado hasta ahora)

L  E  A  N
S  E  T  M
T  S  B  D
I  E  G  O

Score: 830
Words: 229
SLEETIEST  MANTELETS
MANTEELS  MANTELET  MATELESS
MANTEEL  MANTELS  TESTEES  BETISES  OBTESTS  OBESEST
SLEETS  SLEEST  TESTIS  TESTES  TSETSE  MANTES  MANTEL  TESTAE  TESTEE
STEELS  STELES  BETELS  BESETS  BESITS  BETISE  BODGES  BESEES  EISELS
GESTES  GEISTS  OBTEST
LEANT  LEATS  LEETS  LEESE  LESES  LESTS  LESBO  ANTES  NATES  SLEET  SETAE
SEATS  STIES  STEEL  STETS  STEAN  STEAM  STELE  SELES  TAELS  TEELS  TESTS
TESTE  TELES  TETES  MATES  TESTA  TEATS  SEELS  SITES  BEETS  BETEL  BETES
BESET  BESTS  BESIT  BEATS  BODGE  BESEE  DOGES  EISEL  GESTS  GESTE  GESSE
GEITS  GEIST  OBESE
LEAN  LEAT  LEAM  LEET  LEES  LETS  LEST  LESS  EATS  EELS  ELSE  ETNA  ESES
ESTS  ESSE  ANTE  ANTS  ATES  AMBO  NATS  SLEE  SEEL  SETA  SETS  SESE  SEAN
SEAT  SEAM  SELE  STIE  STET  SEES  TAEL  TAES  TEEL  TEES  TEST  TEAM  TELE
TELS  TETS  TETE  MATE  MATS  MAES  TIES  TEAT  TEGS  SELS  SEGO  SITS  SITE
BEET  BEES  BETA  BETE  BETS  BEST  BEAN  BEAT  BEAM  BELS  BOGS  BEGO  BEGS
DOGE  DOGS  DOBS  GOBS  GEST  GEIT  GETS  OBES
LEA  LEE  LET  LES  EAN  EAT  EEL  ELS  ETA  EST  ESS  ANT  ATE  NAT  NAE  NAM
SEE  SET  SEA  SEL  TAN  TAE  TAM  TEE  TES  TEA  TEL  TET  MNA  MAN  MAT  MAE
TIE  TIS  TEG  SEG  SEI  SIT  BEE  BET  BEL  BOD  BOG  BEG  DOG  DOB  ITS  EGO
GOD  GOB  GET  OBS  OBE
EA  EE  EL  ET  ES  AN  AT  AE  AM  NA  ST  TA  TE  MA
TI  SI  BE  BO  DO  IT  IS  GO  OD  OB
Adam Speight
fuente
Consígueme una máquina con tanta RAM y hablaremos.
breadbox
Debes dividir las permutaciones de dados entre 8 para tener en cuenta las simetrías del cuadrado. Además, ¿cómo se obtiene (4 ^ 4) (5 ^ 6) (6 ^ 5)? Lo hago (4 ^ 3) (5 ^ 7) (6 ^ 6), para un espacio de búsqueda total de poco más de 2 ^ 79.
Peter Taylor
@ Peter Taylor: Tienes razón. Debo haber eliminado uno a muchos, cuando hacen las caras únicas. Creo que podemos estar de acuerdo en que hay 83 caras únicas (excluyendo las repeticiones a través del dado). Elige cualquier 16 sin repeticiones. '83 x 82 x 81 x 80 x 79 x 78 x 77 x 76 x 75 x 74 x 73 x 72 x 71 x 70 x 69 x 68 'Aprox .: 1.082 x (10 ^ 30) ==> ~ 2 ^ 100 Qué siempre lo es, es un gran número.
Adam Speight
2
@AdamSpeight Originalmente asumí que tu comentario sobre almacenar el diccionario como una tabla hash era solo una broma, por lo que básicamente lo ignoré. Mis disculpas. Una respuesta adecuada sería: en realidad, una tabla hash es una estructura de datos pésima para este problema. Solo puede responder la pregunta "¿X es una palabra válida?", Por lo que debe construir todas las cadenas posibles para encontrar las palabras. Un DAWG le permite preguntar "¿es X un prefijo de cualquier palabra válida? Y si es así, ¿qué letras pueden seguirlo?" Esto le permite podar el espacio de búsqueda a una pequeña fracción de su tamaño total.
breadbox
Hashtable es terrible ya que evita que elimines fragmentos de palabras que nunca se convertirán en palabras completas (dicttree.ceiling (fragment) .startsWith (fragment)). Aunque cualquier tablero de boggle tiene muchos millones de palabras potenciales, puede tirar una gran parte de ellas después de haber unido 2-3 letras. El recorrido del árbol es más lento que la búsqueda de tabla hash, pero el árbol le permite evitar más del 99 por ciento del trabajo a través del retroceso.
Jim W
1

Mi entrada está aquí en Dream.In.Code ~ 30ms por búsqueda de placa (en una máquina de 2 núcleos, debería ser más rápida con más núcleos)

Adam Speight
fuente
Todavía estás en ella, pero el primer eslabón en esa página no se encuentra el :en http://. ;-)
Gaffi
Muy agradable. Intentaré robar eso para mí como una experiencia de aprendizaje. .NETque VBAno es demasiado duro.
Gaffi
¿Le importaría actualizar la respuesta para incluir su puntaje promedio al ejecutar la lista ISPELL (no SOWPODS)? Eso es parte del desafío, y estoy interesado en ver cómo se comparan sus resultados con los de breadbox.
Gaffi