Acho que você provavelmente passará a maior parte do tempo tentando combinar palavras que não podem ser construídas pela grade de letras. Então, a primeira coisa que eu faria é tentar acelerar essa etapa e isso deve levá-lo até lá.
Para isso, eu expressaria novamente a grade como uma tabela de possíveis "movimentos" que você indexa pela transição de letras que está observando.
Comece atribuindo a cada letra um número do alfabeto inteiro (A = 0, B = 1, C = 2, ... e assim por diante).
Vamos pegar este exemplo:
h b c d
e e g h
l l k l
m o f p
E, por enquanto, vamos usar o alfabeto das letras que temos (normalmente você provavelmente desejaria usar o mesmo alfabeto todo o tempo):
b | c | d | e | f | g | h | k | l | m | o | p
---+---+---+---+---+---+---+---+---+---+----+----
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11
Em seguida, você cria uma matriz booleana 2D que informa se você tem uma certa transição de letra disponível:
| 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
Agora passe pela sua lista de palavras e converta as palavras em transições:
hello (6, 3, 8, 8, 10):
6 -> 3, 3 -> 8, 8 -> 8, 8 -> 10
Em seguida, verifique se essas transições são permitidas procurando-as na sua tabela:
[6][ 3] : T
[3][ 8] : T
[8][ 8] : T
[8][10] : T
Se todos forem permitidos, é possível que essa palavra seja encontrada.
Por exemplo, a palavra "capacete" pode ser descartada na quarta transição (m para e: helMEt), pois essa entrada na sua tabela é falsa.
E a palavra hamster pode ser descartada, uma vez que a primeira transição (h para a) não é permitida (nem existe na sua tabela).
Agora, para as provavelmente poucas palavras restantes que você não eliminou, tente encontrá-las na grade da maneira que você está fazendo agora ou como sugerido em algumas das outras respostas aqui. Isso evita falsos positivos resultantes de saltos entre letras idênticas em sua grade. Por exemplo, a palavra "ajuda" é permitida pela tabela, mas não pela grade.
Mais algumas dicas de melhoria de desempenho sobre essa ideia:
Em vez de usar uma matriz 2D, use uma matriz 1D e simplesmente calcule o índice da segunda letra. Portanto, em vez de uma matriz de 12x12 como acima, faça uma matriz 1D de comprimento 144. Se você sempre usar o mesmo alfabeto (ou seja, uma matriz 26x26 = 676x1 para o alfabeto inglês padrão), mesmo que nem todas as letras apareçam na sua grade , você pode pré-calcular os índices nessa matriz 1D que precisa testar para corresponder às palavras do seu dicionário. Por exemplo, os índices para 'olá' no exemplo acima seriam
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
Estenda a ideia a uma tabela 3D (expressa como uma matriz 1D), ou seja, todas as combinações de 3 letras permitidas. Dessa forma, você pode eliminar ainda mais palavras imediatamente e reduzir o número de pesquisas de matriz para cada palavra em 1: Para 'hello', você precisa apenas de 3 pesquisas de matriz: hel, ell, llo. A propósito, será muito rápido criar esta tabela, pois existem apenas 400 movimentos possíveis de três letras em sua grade.
Pré-calcule os índices dos movimentos na sua grade que você precisa incluir na sua tabela. Para o exemplo acima, você precisa definir as seguintes entradas como 'True':
(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]
.
:
- Também represente sua grade de jogo em uma matriz 1-D com 16 entradas e faça com que a tabela pré-calculada em 3. contenha os índices nessa matriz.
Tenho certeza de que, se você usar essa abordagem, poderá fazer com que seu código funcione incrivelmente rápido, se você tiver o dicionário pré-calculado e já carregado na memória.
BTW: Outra coisa legal a se fazer, se você estiver criando um jogo, é executar esse tipo de coisa imediatamente em segundo plano. Comece a gerar e resolver o primeiro jogo enquanto o usuário ainda estiver olhando para a tela de título do seu aplicativo e colocando o dedo na posição para pressionar "Play". Em seguida, gere e resolva o próximo jogo conforme o usuário joga o anterior. Isso deve lhe dar muito tempo para executar seu código.
(Eu gosto desse problema, provavelmente terei a tentação de implementar minha proposta em Java nos próximos dias para ver como ela realmente funcionaria ... Eu publicarei o código aqui assim que o fizer.)
ATUALIZAR:
Ok, eu tive algum tempo hoje e implementei essa idéia em 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();
}
}
Aqui estão alguns resultados:
Para a grade da imagem postada na pergunta 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 as cartas postadas como exemplo na pergunta 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 a seguinte grade 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
dá o seguinte:
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 isso, usei a Lista de Palavras Scrabble do Torneio TWL06 , já que o link na pergunta original não funciona mais. Este arquivo tem 1,85 MB, por isso é um pouco mais curto. E a buildDictionary
função lança todas as palavras com menos de 3 letras.
Aqui estão algumas observações sobre o desempenho disso:
É cerca de 10 vezes mais lento que o desempenho relatado da implementação do OCaml de Victor Nicollet. Se isso é causado pelo algoritmo diferente, pelo dicionário mais curto que ele usou, pelo fato de seu código ser compilado e o meu ser executado em uma máquina virtual Java ou pelo desempenho de nossos computadores (o meu é um Intel Q6600 @ 2.4MHz executando o WinXP), Eu não sei. Mas é muito mais rápido que os resultados das outras implementações citadas no final da pergunta original. Portanto, se esse algoritmo é superior ao dicionário trie ou não, não sei neste momento.
O método de tabela usado em checkWordTriplets()
produz uma aproximação muito boa às respostas reais. Somente 1 em 3 a 5 palavras aprovadas serão reprovadas no checkWords()
teste (consulte o número de candidatos versus o número de palavras reais acima).
Algo que você não pode ver acima: a checkWordTriplets()
função leva cerca de 3,65 ms e, portanto, é totalmente dominante no processo de busca. A checkWords()
função ocupa praticamente os restantes 0,05-0,20 ms.
O tempo de execução da checkWordTriplets()
função depende linearmente do tamanho do dicionário e é praticamente independente do tamanho da placa!
O tempo de execução checkWords()
depende do tamanho do quadro e do número de palavras não descartadas checkWordTriplets()
.
A checkWords()
implementação acima é a primeira versão mais idiota que eu criei. Basicamente, não é otimizado. Mas, comparado a checkWordTriplets()
isso, é irrelevante para o desempenho total do aplicativo, então não me preocupei. Porém , se o tamanho da placa aumentar, essa função ficará cada vez mais lenta e, eventualmente, começará a importar. Então, precisaria ser otimizado também.
Uma coisa legal desse código é sua flexibilidade:
- Você pode alterar facilmente o tamanho da placa: Atualize a linha 10 e a matriz String transmitida para
initializeBoard()
.
- Ele suporta alfabetos maiores / diferentes e pode lidar com coisas como tratar 'Qu' como uma letra sem sobrecarga de desempenho. Para fazer isso, seria necessário atualizar a linha 9 e o par de lugares em que os caracteres são convertidos em números (atualmente simplesmente subtraindo 65 do valor ASCII)
Ok, mas acho que até agora este post está bom o suficiente. Definitivamente, posso responder a quaisquer perguntas que você possa ter, mas vamos passar para os comentários.