Creo que probablemente pasarás la mayor parte de tu tiempo tratando de unir palabras que posiblemente no se pueden construir con tu cuadrícula de letras. Entonces, lo primero que haría es tratar de acelerar ese paso y eso debería llevarlo a la mayor parte del camino.
Para esto, volvería a expresar la cuadrícula como una tabla de posibles "movimientos" que indices según la transición de letras que estás viendo.
Comience asignando a cada letra un número de su alfabeto completo (A = 0, B = 1, C = 2, ... y así sucesivamente).
Tomemos este ejemplo:
h b c d
e e g h
l l k l
m o f p
Y por ahora, usemos el alfabeto de las letras que tenemos (por lo general, probablemente quieras usar el mismo alfabeto completo cada vez):
b | c | d | e | f | g | h | k | l | m | o | p
---+---+---+---+---+---+---+---+---+---+----+----
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11
Luego crea una matriz booleana 2D que le indica si tiene disponible una determinada transición de letras:
| 0 1 2 3 4 5 6 7 8 9 10 11 <- from letter
| b c d e f g h k l m o p
-----+--------------------------------------
0 b | T T T T
1 c | T T T T T
2 d | T T T
3 e | T T T T T T T
4 f | T T T T
5 g | T T T T T T T
6 h | T T T T T T T
7 k | T T T T T T T
8 l | T T T T T T T T T
9 m | T T
10 o | T T T T
11 p | T T T
^
to letter
Ahora revisa tu lista de palabras y convierte las palabras en transiciones:
hello (6, 3, 8, 8, 10):
6 -> 3, 3 -> 8, 8 -> 8, 8 -> 10
Luego verifique si estas transiciones están permitidas buscándolas en su tabla:
[6][ 3] : T
[3][ 8] : T
[8][ 8] : T
[8][10] : T
Si todos están permitidos, existe la posibilidad de que se encuentre esta palabra.
Por ejemplo, la palabra "casco" se puede descartar en la cuarta transición (m to e: helMEt), ya que esa entrada en su tabla es falsa.
Y la palabra hámster se puede descartar, ya que la primera transición (h a a) no está permitida (ni siquiera existe en su tabla).
Ahora, para las pocas palabras restantes que probablemente no eliminó, intente encontrarlas realmente en la cuadrícula de la forma en que lo hace ahora o como se sugiere en algunas de las otras respuestas aquí. Esto es para evitar falsos positivos que resultan de saltos entre letras idénticas en su cuadrícula. Por ejemplo, la palabra "ayuda" está permitida por la tabla, pero no por la cuadrícula.
Algunos consejos adicionales para mejorar el rendimiento de esta idea:
En lugar de usar una matriz 2D, use una matriz 1D y simplemente calcule el índice de la segunda letra usted mismo. Entonces, en lugar de una matriz de 12x12 como la anterior, haga una matriz 1D de longitud 144. Si usa siempre el mismo alfabeto (es decir, una matriz 26x26 = 676x1 para el alfabeto inglés estándar), incluso si no aparecen todas las letras en su cuadrícula , puede calcular previamente los índices en esta matriz 1D que necesita probar para que coincidan con las palabras del diccionario. Por ejemplo, los índices para 'hola' en el ejemplo anterior serían
hello (6, 3, 8, 8, 10):
42 (from 6 + 3x12), 99, 104, 128
-> "hello" will be stored as 42, 99, 104, 128 in the dictionary
Extienda la idea a una tabla 3D (expresada como una matriz 1D), es decir, todas las combinaciones de 3 letras permitidas. De esa manera, puede eliminar aún más palabras de inmediato y reducir el número de búsquedas de matriz para cada palabra en 1: para 'hola', solo necesita 3 búsquedas de matriz: hel, ell, llo. Por cierto, será muy rápido construir esta tabla, ya que solo hay 400 movimientos posibles de 3 letras en su cuadrícula.
Precalcule los índices de los movimientos en su cuadrícula que necesita incluir en su tabla. Para el ejemplo anterior, debe establecer las siguientes entradas en 'Verdadero':
(0,0) (0,1) -> here: h, b : [6][0]
(0,0) (1,0) -> here: h, e : [6][3]
(0,0) (1,1) -> here: h, e : [6][3]
(0,1) (0,0) -> here: b, h : [0][6]
(0,1) (0,2) -> here: b, c : [0][1]
.
:
- También represente su cuadrícula de juego en una matriz 1-D con 16 entradas y haga que la tabla precalculada en 3. contenga los índices en esta matriz.
Estoy seguro de que si usa este enfoque, puede hacer que su código se ejecute increíblemente rápido, si tiene el diccionario precalculado y ya cargado en la memoria.
Por cierto: Otra buena cosa que hacer, si estás creando un juego, es ejecutar este tipo de cosas inmediatamente en segundo plano. Comience a generar y resolver el primer juego mientras el usuario sigue mirando la pantalla de título de su aplicación y coloca el dedo en posición para presionar "Reproducir". Luego genera y resuelve el siguiente juego mientras el usuario juega el anterior. Eso debería darle mucho tiempo para ejecutar su código.
(Me gusta este problema, así que probablemente estaré tentado de implementar mi propuesta en Java en algún momento en los próximos días para ver cómo funcionaría realmente ... Publicaré el código aquí una vez que lo haga).
ACTUALIZAR:
Ok, tuve algo de tiempo hoy e implementé esta idea en Java:
class DictionaryEntry {
public int[] letters;
public int[] triplets;
}
class BoggleSolver {
// Constants
final int ALPHABET_SIZE = 5; // up to 2^5 = 32 letters
final int BOARD_SIZE = 4; // 4x4 board
final int[] moves = {-BOARD_SIZE-1, -BOARD_SIZE, -BOARD_SIZE+1,
-1, +1,
+BOARD_SIZE-1, +BOARD_SIZE, +BOARD_SIZE+1};
// Technically constant (calculated here for flexibility, but should be fixed)
DictionaryEntry[] dictionary; // Processed word list
int maxWordLength = 0;
int[] boardTripletIndices; // List of all 3-letter moves in board coordinates
DictionaryEntry[] buildDictionary(String fileName) throws IOException {
BufferedReader fileReader = new BufferedReader(new FileReader(fileName));
String word = fileReader.readLine();
ArrayList<DictionaryEntry> result = new ArrayList<DictionaryEntry>();
while (word!=null) {
if (word.length()>=3) {
word = word.toUpperCase();
if (word.length()>maxWordLength) maxWordLength = word.length();
DictionaryEntry entry = new DictionaryEntry();
entry.letters = new int[word.length() ];
entry.triplets = new int[word.length()-2];
int i=0;
for (char letter: word.toCharArray()) {
entry.letters[i] = (byte) letter - 65; // Convert ASCII to 0..25
if (i>=2)
entry.triplets[i-2] = (((entry.letters[i-2] << ALPHABET_SIZE) +
entry.letters[i-1]) << ALPHABET_SIZE) +
entry.letters[i];
i++;
}
result.add(entry);
}
word = fileReader.readLine();
}
return result.toArray(new DictionaryEntry[result.size()]);
}
boolean isWrap(int a, int b) { // Checks if move a->b wraps board edge (like 3->4)
return Math.abs(a%BOARD_SIZE-b%BOARD_SIZE)>1;
}
int[] buildTripletIndices() {
ArrayList<Integer> result = new ArrayList<Integer>();
for (int a=0; a<BOARD_SIZE*BOARD_SIZE; a++)
for (int bm: moves) {
int b=a+bm;
if ((b>=0) && (b<board.length) && !isWrap(a, b))
for (int cm: moves) {
int c=b+cm;
if ((c>=0) && (c<board.length) && (c!=a) && !isWrap(b, c)) {
result.add(a);
result.add(b);
result.add(c);
}
}
}
int[] result2 = new int[result.size()];
int i=0;
for (Integer r: result) result2[i++] = r;
return result2;
}
// Variables that depend on the actual game layout
int[] board = new int[BOARD_SIZE*BOARD_SIZE]; // Letters in board
boolean[] possibleTriplets = new boolean[1 << (ALPHABET_SIZE*3)];
DictionaryEntry[] candidateWords;
int candidateCount;
int[] usedBoardPositions;
DictionaryEntry[] foundWords;
int foundCount;
void initializeBoard(String[] letters) {
for (int row=0; row<BOARD_SIZE; row++)
for (int col=0; col<BOARD_SIZE; col++)
board[row*BOARD_SIZE + col] = (byte) letters[row].charAt(col) - 65;
}
void setPossibleTriplets() {
Arrays.fill(possibleTriplets, false); // Reset list
int i=0;
while (i<boardTripletIndices.length) {
int triplet = (((board[boardTripletIndices[i++]] << ALPHABET_SIZE) +
board[boardTripletIndices[i++]]) << ALPHABET_SIZE) +
board[boardTripletIndices[i++]];
possibleTriplets[triplet] = true;
}
}
void checkWordTriplets() {
candidateCount = 0;
for (DictionaryEntry entry: dictionary) {
boolean ok = true;
int len = entry.triplets.length;
for (int t=0; (t<len) && ok; t++)
ok = possibleTriplets[entry.triplets[t]];
if (ok) candidateWords[candidateCount++] = entry;
}
}
void checkWords() { // Can probably be optimized a lot
foundCount = 0;
for (int i=0; i<candidateCount; i++) {
DictionaryEntry candidate = candidateWords[i];
for (int j=0; j<board.length; j++)
if (board[j]==candidate.letters[0]) {
usedBoardPositions[0] = j;
if (checkNextLetters(candidate, 1, j)) {
foundWords[foundCount++] = candidate;
break;
}
}
}
}
boolean checkNextLetters(DictionaryEntry candidate, int letter, int pos) {
if (letter==candidate.letters.length) return true;
int match = candidate.letters[letter];
for (int move: moves) {
int next=pos+move;
if ((next>=0) && (next<board.length) && (board[next]==match) && !isWrap(pos, next)) {
boolean ok = true;
for (int i=0; (i<letter) && ok; i++)
ok = usedBoardPositions[i]!=next;
if (ok) {
usedBoardPositions[letter] = next;
if (checkNextLetters(candidate, letter+1, next)) return true;
}
}
}
return false;
}
// Just some helper functions
String formatTime(long start, long end, long repetitions) {
long time = (end-start)/repetitions;
return time/1000000 + "." + (time/100000) % 10 + "" + (time/10000) % 10 + "ms";
}
String getWord(DictionaryEntry entry) {
char[] result = new char[entry.letters.length];
int i=0;
for (int letter: entry.letters)
result[i++] = (char) (letter+97);
return new String(result);
}
void run() throws IOException {
long start = System.nanoTime();
// The following can be pre-computed and should be replaced by constants
dictionary = buildDictionary("C:/TWL06.txt");
boardTripletIndices = buildTripletIndices();
long precomputed = System.nanoTime();
// The following only needs to run once at the beginning of the program
candidateWords = new DictionaryEntry[dictionary.length]; // WAAAY too generous
foundWords = new DictionaryEntry[dictionary.length]; // WAAAY too generous
usedBoardPositions = new int[maxWordLength];
long initialized = System.nanoTime();
for (int n=1; n<=100; n++) {
// The following needs to run again for every new board
initializeBoard(new String[] {"DGHI",
"KLPS",
"YEUT",
"EORN"});
setPossibleTriplets();
checkWordTriplets();
checkWords();
}
long solved = System.nanoTime();
// Print out result and statistics
System.out.println("Precomputation finished in " + formatTime(start, precomputed, 1)+":");
System.out.println(" Words in the dictionary: "+dictionary.length);
System.out.println(" Longest word: "+maxWordLength+" letters");
System.out.println(" Number of triplet-moves: "+boardTripletIndices.length/3);
System.out.println();
System.out.println("Initialization finished in " + formatTime(precomputed, initialized, 1));
System.out.println();
System.out.println("Board solved in "+formatTime(initialized, solved, 100)+":");
System.out.println(" Number of candidates: "+candidateCount);
System.out.println(" Number of actual words: "+foundCount);
System.out.println();
System.out.println("Words found:");
int w=0;
System.out.print(" ");
for (int i=0; i<foundCount; i++) {
System.out.print(getWord(foundWords[i]));
w++;
if (w==10) {
w=0;
System.out.println(); System.out.print(" ");
} else
if (i<foundCount-1) System.out.print(", ");
}
System.out.println();
}
public static void main(String[] args) throws IOException {
new BoggleSolver().run();
}
}
Aquí hay algunos resultados:
Para la cuadrícula de la imagen publicada en la pregunta original (DGHI ...):
Precomputation finished in 239.59ms:
Words in the dictionary: 178590
Longest word: 15 letters
Number of triplet-moves: 408
Initialization finished in 0.22ms
Board solved in 3.70ms:
Number of candidates: 230
Number of actual words: 163
Words found:
eek, eel, eely, eld, elhi, elk, ern, erupt, erupts, euro
eye, eyer, ghi, ghis, glee, gley, glue, gluer, gluey, glut
gluts, hip, hiply, hips, his, hist, kelp, kelps, kep, kepi
kepis, keps, kept, kern, key, kye, lee, lek, lept, leu
ley, lunt, lunts, lure, lush, lust, lustre, lye, nus, nut
nuts, ore, ort, orts, ouph, ouphs, our, oust, out, outre
outs, oyer, pee, per, pert, phi, phis, pis, pish, plus
plush, ply, plyer, psi, pst, pul, pule, puler, pun, punt
punts, pur, pure, puree, purely, pus, push, put, puts, ree
rely, rep, reply, reps, roe, roue, roup, roups, roust, rout
routs, rue, rule, ruly, run, runt, runts, rupee, rush, rust
rut, ruts, ship, shlep, sip, sipe, spue, spun, spur, spurn
spurt, strep, stroy, stun, stupe, sue, suer, sulk, sulker, sulky
sun, sup, supe, super, sure, surely, tree, trek, trey, troupe
troy, true, truly, tule, tun, tup, tups, turn, tush, ups
urn, uts, yeld, yelk, yelp, yelps, yep, yeps, yore, you
your, yourn, yous
Para las cartas publicadas como ejemplo en la pregunta original (FXIE ...)
Precomputation finished in 239.68ms:
Words in the dictionary: 178590
Longest word: 15 letters
Number of triplet-moves: 408
Initialization finished in 0.21ms
Board solved in 3.69ms:
Number of candidates: 87
Number of actual words: 76
Words found:
amble, ambo, ami, amie, asea, awa, awe, awes, awl, axil
axile, axle, boil, bole, box, but, buts, east, elm, emboli
fame, fames, fax, lei, lie, lima, limb, limbo, limbs, lime
limes, lob, lobs, lox, mae, maes, maw, maws, max, maxi
mesa, mew, mewl, mews, mil, mile, milo, mix, oil, ole
sae, saw, sea, seam, semi, sew, stub, swam, swami, tub
tubs, tux, twa, twae, twaes, twas, uts, wae, waes, wamble
wame, wames, was, wast, wax, west
Para la siguiente cuadrícula de 5x5:
R P R I T
A H H L N
I E T E P
Z R Y S G
O G W E Y
da esto:
Precomputation finished in 240.39ms:
Words in the dictionary: 178590
Longest word: 15 letters
Number of triplet-moves: 768
Initialization finished in 0.23ms
Board solved in 3.85ms:
Number of candidates: 331
Number of actual words: 240
Words found:
aero, aery, ahi, air, airt, airth, airts, airy, ear, egest
elhi, elint, erg, ergo, ester, eth, ether, eye, eyen, eyer
eyes, eyre, eyrie, gel, gelt, gelts, gen, gent, gentil, gest
geste, get, gets, gey, gor, gore, gory, grey, greyest, greys
gyre, gyri, gyro, hae, haet, haets, hair, hairy, hap, harp
heap, hear, heh, heir, help, helps, hen, hent, hep, her
hero, hes, hest, het, hetero, heth, hets, hey, hie, hilt
hilts, hin, hint, hire, hit, inlet, inlets, ire, leg, leges
legs, lehr, lent, les, lest, let, lethe, lets, ley, leys
lin, line, lines, liney, lint, lit, neg, negs, nest, nester
net, nether, nets, nil, nit, ogre, ore, orgy, ort, orts
pah, pair, par, peg, pegs, peh, pelt, pelter, peltry, pelts
pen, pent, pes, pest, pester, pesty, pet, peter, pets, phi
philter, philtre, phiz, pht, print, pst, rah, rai, rap, raphe
raphes, reap, rear, rei, ret, rete, rets, rhaphe, rhaphes, rhea
ria, rile, riles, riley, rin, rye, ryes, seg, sel, sen
sent, senti, set, sew, spelt, spelter, spent, splent, spline, splint
split, stent, step, stey, stria, striae, sty, stye, tea, tear
teg, tegs, tel, ten, tent, thae, the, their, then, these
thesp, they, thin, thine, thir, thirl, til, tile, tiles, tilt
tilter, tilth, tilts, tin, tine, tines, tirl, trey, treys, trog
try, tye, tyer, tyes, tyre, tyro, west, wester, wry, wryest
wye, wyes, wyte, wytes, yea, yeah, year, yeh, yelp, yelps
yen, yep, yeps, yes, yester, yet, yew, yews, zero, zori
Para esto utilicé la Lista de palabras de Scrabble del torneo TWL06 , ya que el enlace en la pregunta original ya no funciona. Este archivo tiene 1,85 MB, por lo que es un poco más corto. Y la buildDictionary
función arroja todas las palabras con menos de 3 letras.
Aquí hay un par de observaciones sobre el desempeño de esto:
Es aproximadamente 10 veces más lento que el rendimiento reportado de la implementación OCaml de Victor Nicollet. Si esto es causado por el algoritmo diferente, el diccionario más corto que utilizó, el hecho de que su código está compilado y el mío se ejecuta en una máquina virtual Java, o el rendimiento de nuestras computadoras (el mío es un Intel Q6600 @ 2.4MHz con WinXP), No lo sé. Pero es mucho más rápido que los resultados para las otras implementaciones citadas al final de la pregunta original. Entonces, si este algoritmo es superior al diccionario trie o no, no lo sé en este momento.
El método de tabla utilizado en checkWordTriplets()
produce una muy buena aproximación a las respuestas reales. Solo 1 de cada 3-5 palabras aprobadas no pasará la checkWords()
prueba (ver el número de candidatos versus el número de palabras reales arriba).
Algo que no puede ver arriba: la checkWordTriplets()
función tarda unos 3,65 ms y, por lo tanto, es totalmente dominante en el proceso de búsqueda. La checkWords()
función ocupa más o menos los 0.05-0.20 ms restantes.
¡El tiempo de ejecución de la checkWordTriplets()
función depende linealmente del tamaño del diccionario y es prácticamente independiente del tamaño del tablero!
El tiempo de ejecución de checkWords()
depende del tamaño del tablero y del número de palabras que no se descartan checkWordTriplets()
.
La checkWords()
implementación anterior es la primera versión más tonta que se me ocurrió. Básicamente no está optimizado en absoluto. Pero en comparación con checkWordTriplets()
esto es irrelevante para el rendimiento total de la aplicación, así que no me preocupé por eso. Pero , si el tamaño de la placa aumenta, esta función se volverá cada vez más lenta y eventualmente comenzará a tener importancia. Entonces, también necesitaría ser optimizado.
Una cosa buena de este código es su flexibilidad:
- Puede cambiar fácilmente el tamaño de la placa: actualice la línea 10 y el conjunto de cadenas pasado
initializeBoard()
.
- Puede admitir alfabetos más grandes / diferentes y puede manejar cosas como tratar 'Qu' como una letra sin ninguna sobrecarga de rendimiento. Para hacer esto, uno necesitaría actualizar la línea 9 y el par de lugares donde los caracteres se convierten en números (actualmente simplemente restando 65 del valor ASCII)
Ok, pero creo que esta publicación ya es muuuuucho tiempo suficiente. Definitivamente puedo responder cualquier pregunta que pueda tener, pero pasemos a los comentarios.