Comment trouver la liste des mots possibles à partir d'une matrice de lettres [Boggle Solver]

376

Dernièrement, j'ai joué à un jeu sur mon iPhone appelé Scramble. Certains d'entre vous connaissent peut-être ce jeu sous le nom de Boggle. Essentiellement, lorsque le jeu commence, vous obtenez une matrice de lettres comme ceci:

F X I E
A M L O
E W B X
A S T U

Le but du jeu est de trouver autant de mots que possible qui peuvent être formés en enchaînant des lettres. Vous pouvez commencer par n'importe quelle lettre, et toutes les lettres qui l'entourent sont du jeu équitable, puis une fois que vous passez à la lettre suivante, toutes les lettres qui entourent cette lettre sont du jeu équitable, à l' exception des lettres précédemment utilisées . Ainsi , dans la grille ci - dessus, par exemple, je pourrais venir avec les mots LOB, TUX, SEA, FAME, etc. Les mots doivent être au moins 3 caractères et pas plus de caractères NxN, qui serait 16 dans ce jeu , mais peut varier dans certaines implémentations . Bien que ce jeu soit amusant et addictif, je ne suis apparemment pas très bon et je voulais tricher un peu en créant un programme qui me donnerait les meilleurs mots possibles (plus le mot est long, plus vous obtenez de points).

Exemple Boggle
(source: boggled.org )

Je ne suis malheureusement pas très bon avec les algorithmes ou leur efficacité et ainsi de suite. Ma première tentative utilise un dictionnaire tel que celui-ci (~ 2,3 Mo) et effectue une recherche linéaire en essayant de faire correspondre les combinaisons avec les entrées du dictionnaire. Cela prend beaucoup de temps pour trouver les mots possibles, et comme vous ne disposez que de 2 minutes par tour, ce n'est tout simplement pas suffisant.

Je suis intéressé de voir si des Stackoverflowers peuvent proposer des solutions plus efficaces. Je recherche principalement des solutions utilisant les Big 3 Ps: Python, PHP et Perl, bien que tout avec Java ou C ++ soit cool aussi, car la vitesse est essentielle.

SOLUTIONS ACTUELLES :

  • Adam Rosenfield, Python, ~ 20s
  • John Fouhy, Python, ~ 3s
  • Kent Fredric, Perl, ~ 1 s
  • Bacon Darius, Python, ~ 1 s
  • rvarcher, VB.NET (lien en direct) , ~ 1s
  • Paolo Bergantino, PHP (lien en direct) , ~ 5s (~ 2s localement)
Paolo Bergantino
la source
18
demande de fonctionnalité MOAR PUZZLES
Kent Fredric
6
En ce qui concerne les délais: dans ma solution, presque tout le temps est consacré à la construction du trie. Une fois le trie construit, il peut être réutilisé plusieurs fois. Si seulement résoudre un casse-tête, il serait plus efficace d'utiliser une structure de données plus simple (comme un ensemble de tous les mots et de tous les préfixes).
Adam Rosenfield
3
De plus, Adam a un dictionnaire plus grand, comme en témoigne le nombre de mots plus longs que sa solution utilise. Ils doivent tous être testés sur la base d'un dictionnaire commun.
Rich Bradshaw
2
Je suppose que personne ne joue beaucoup Boggle? "Qu" est une "lettre" et je ne sais pas combien de solutions ont saisi ce petit détail. Il semble que certains d'entre eux vous permettent d'utiliser le "u" indépendamment, entre autres problèmes.
Qsario
2
J'ai récemment posé cette question en tant qu'interview et je suis resté bien coincé dans les détails. Je le traitais comme un problème de graphique, ce qui est bien, mais les solutions ici utilisent beaucoup moins d'espace. Je code ma propre solution maintenant. Bravo à tous ceux qui ont contribué!
Peter Friend

Réponses:

143

Ma réponse fonctionne comme les autres ici, mais je la publierai car elle semble un peu plus rapide que les autres solutions Python, de la configuration du dictionnaire plus rapidement. (J'ai vérifié cela par rapport à la solution de John Fouhy.) Après la configuration, le temps de résolution est dans le bruit.

grid = "fxie amlo ewbx astu".split()
nrows, ncols = len(grid), len(grid[0])

# A dictionary word that could be a solution must use only the grid's
# letters and have length >= 3. (With a case-insensitive match.)
import re
alphabet = ''.join(set(''.join(grid)))
bogglable = re.compile('[' + alphabet + ']{3,}$', re.I).match

words = set(word.rstrip('\n') for word in open('words') if bogglable(word))
prefixes = set(word[:i] for word in words
               for i in range(2, len(word)+1))

def solve():
    for y, row in enumerate(grid):
        for x, letter in enumerate(row):
            for result in extending(letter, ((x, y),)):
                yield result

def extending(prefix, path):
    if prefix in words:
        yield (prefix, path)
    for (nx, ny) in neighbors(path[-1]):
        if (nx, ny) not in path:
            prefix1 = prefix + grid[ny][nx]
            if prefix1 in prefixes:
                for result in extending(prefix1, path + ((nx, ny),)):
                    yield result

def neighbors((x, y)):
    for nx in range(max(0, x-1), min(x+2, ncols)):
        for ny in range(max(0, y-1), min(y+2, nrows)):
            yield (nx, ny)

Exemple d'utilisation:

# Print a maximal-length word and its path:
print max(solve(), key=lambda (word, path): len(word))

Modifier: filtrez les mots de moins de 3 lettres.

Edit 2: J'étais curieux de savoir pourquoi la solution Perl de Kent Fredric était plus rapide; il s'avère utiliser une correspondance d'expressions régulières au lieu d'un ensemble de caractères. Faire de même en Python pour doubler la vitesse.

Bacon Darius
la source
Le programme ne me donne qu'un mot. Comment venir?
Paolo Bergantino
Je ne voulais pas me noyer en sortie. Voir le commentaire en bas.
Darius Bacon
6
Ou obtenez tous les mots sans les chemins: imprimez '' .join (trié (défini (mot (mot, chemin)) dans résoudre ())))
Darius Bacon
2
Une grande partie du temps est consacrée à l'analyse du dictionnaire. J'ai pré-analysé cela dans un fichier "wordlines.py" qui est juste une liste avec chaque mot étant un élément. Parce que c'est un fichier .py, cela sera transformé en un fichier .pyc. Alors j'en fais une importation au lieu de read (). Splitlines (). Avec cela, sur ma boîte, je le résous en environ un dixième de seconde.
Sean Reifschneider
1
@shellscape, c'est du code Python 2. Python 3 a abandonné la possibilité de déconstruire les arguments, comme def neighbors((x, y))(inutilement, pour autant que je puisse voir). Il nécessite également des parenthèses autour de l'argument to print.
Darius Bacon
116

La solution la plus rapide que vous obtiendrez consistera probablement à stocker votre dictionnaire dans un trie . Ensuite, créez une file d'attente de triplets ( x , y , s ), où chaque élément de la file d'attente correspond à un préfixe s d'un mot qui peut être orthographié dans la grille, se terminant à l'emplacement ( x , y ). Initialisez la file d'attente avec N x N éléments (où N est la taille de votre grille), un élément pour chaque carré de la grille. Ensuite, l'algorithme procède comme suit:

Alors que la file d'attente n'est pas vide:
  Supprimer un triple (x, y, s)
  Pour chaque carré (x ', y') avec la lettre c adjacente à (x, y):
    Si s + c est un mot, sortez s + c
    Si s + c est le préfixe d'un mot, insérez (x ', y', s + c) dans la file d'attente

Si vous stockez votre dictionnaire dans un trie, tester si s + c est un mot ou un préfixe d'un mot peut être fait en temps constant (à condition de conserver également des métadonnées supplémentaires dans chaque donnée de file d'attente, comme un pointeur vers le nœud actuel dans le trie), donc le temps d'exécution de cet algorithme est O (nombre de mots qui peuvent être épelés).

[Edit] Voici une implémentation en Python que je viens de coder:

#!/usr/bin/python

class TrieNode:
    def __init__(self, parent, value):
        self.parent = parent
        self.children = [None] * 26
        self.isWord = False
        if parent is not None:
            parent.children[ord(value) - 97] = self

def MakeTrie(dictfile):
    dict = open(dictfile)
    root = TrieNode(None, '')
    for word in dict:
        curNode = root
        for letter in word.lower():
            if 97 <= ord(letter) < 123:
                nextNode = curNode.children[ord(letter) - 97]
                if nextNode is None:
                    nextNode = TrieNode(curNode, letter)
                curNode = nextNode
        curNode.isWord = True
    return root

def BoggleWords(grid, dict):
    rows = len(grid)
    cols = len(grid[0])
    queue = []
    words = []
    for y in range(cols):
        for x in range(rows):
            c = grid[y][x]
            node = dict.children[ord(c) - 97]
            if node is not None:
                queue.append((x, y, c, node))
    while queue:
        x, y, s, node = queue[0]
        del queue[0]
        for dx, dy in ((1, 0), (1, -1), (0, -1), (-1, -1), (-1, 0), (-1, 1), (0, 1), (1, 1)):
            x2, y2 = x + dx, y + dy
            if 0 <= x2 < cols and 0 <= y2 < rows:
                s2 = s + grid[y2][x2]
                node2 = node.children[ord(grid[y2][x2]) - 97]
                if node2 is not None:
                    if node2.isWord:
                        words.append(s2)
                    queue.append((x2, y2, s2, node2))

    return words

Exemple d'utilisation:

d = MakeTrie('/usr/share/dict/words')
print(BoggleWords(['fxie','amlo','ewbx','astu'], d))

Production:

['fa', 'xi', 'ie', 'io', 'el', 'am', 'ax', 'ae', 'aw', 'mi', 'ma', 'me', ' lo ',' li ',' oe ',' ox ',' em ',' ea ',' ea ',' es ',' wa ',' we ',' wa ',' bo ',' bu ' , 'as', 'aw', 'ae', 'st', 'se', 'sa', 'tu', 'ut', 'fam', 'fae', 'imi', 'eli', ' orme ',' elb ',' ami ',' ama ',' ame ',' aes ',' awl ',' awa ',' awe ',' awa ',' mix ',' mim ',' mil ' , 'mam', 'max', 'mae', 'maw', 'mew', 'mem', 'mes', 'lob', 'lox', 'lei ',' leo ',' lie ',' lim ',' oil ',' olm ',' ewe ',' eme ',' wax ',' waf ',' wae ',' waw ',' wem ' , 'wea', 'wea', 'was', 'waw', 'wae', 'bob', 'blo', 'bub', 'but', 'ast', 'ase', 'asa', ' poinçon ',' awa ',' awe ',' awa ',' aes ',' swa ',' swa ',' couture ',' sea ',' sea ',' saw ',' tux ',' tub ' , 'tut', 'twa', 'twa', 'tst', 'utu', 'fama', 'fame', 'ixil', 'imam', 'amli', 'amil', 'ambo', ' axil »,« essieu »,« mimi »,« mima »,« mime »,« milo »,»mile ',' mewl ',' mese ',' mesa ',' lolo ',' lobo ',' lima ',' lime ',' limb ',' lile ',' oime ',' oleo ',' olio ' , 'hautbois', 'hautbois', 'emim', 'emil', 'est', 'facilité', 'wame', 'wawa', 'wawa', 'weam', 'west', 'wese', ' wast ',' wase ',' wawa ',' wawa ',' boil ',' bolo ',' bole ',' bobo ',' blob ',' bleo ',' bubo ',' asem ',' stub ' , 'stut', 'swam', 'semi', 'seme', 'seam', 'seax', 'sasa', 'sawt', 'tutu', 'tuts', 'twae', 'twas', ' twae ',' ilima ',' amble ',' axile ', 'awest', 'mamie', 'mambo', 'maxim', 'mease', 'mesem', 'limax', 'limes', 'limbo', 'limbu', 'obole', 'emesa', ' embox ',' awest ',' swami ',' famble ',' mimble ',' maxima ',' embolo ',' embole ',' wamble ',' semese ',' semble ',' sawbwa ',' sawbwa ' ]sawbwa ']sawbwa ']

Remarques: Ce programme ne produit pas de mots à 1 lettre ou ne filtre pas du tout par longueur de mot. C'est facile à ajouter mais pas vraiment pertinent au problème. Il génère également plusieurs mots plusieurs fois s'ils peuvent être orthographiés de plusieurs manières. Si un mot donné peut être orthographié de différentes manières (pire des cas: chaque lettre de la grille est la même (par exemple 'A') et un mot comme 'aaaaaaaaaa' est dans votre dictionnaire), alors le temps d'exécution deviendra horriblement exponentiel . Le filtrage des doublons et du tri est trivial car une fois l'algorithme terminé.

Adam Rosenfield
la source
14
Oooh. Je suis content que quelqu'un soit intervenu. Bien que cela fonctionne, il ne se souvient pas de la lettre qu'il a déjà utilisée et propose des mots qui nécessiteraient d'utiliser deux fois la même lettre, ce qui n'est pas autorisé. Comme je suis un idiot, comment pourrais-je résoudre ce problème?
Paolo Bergantino
3
Certes, il ne se souvient pas quelles lettres ont été visitées, mais cela n'a pas été spécifié dans votre spec =). Pour résoudre ce problème, vous devez ajouter à chaque référence de file d'attente une liste de tous les emplacements visités, puis vérifier cette liste avant d'ajouter le caractère suivant.
Adam Rosenfield
Non, dans BoggleWords (). Au lieu de stocker un quadruplet (x, y, s, n), vous stockeriez un quintuplet (x, y, s, n, l), où l est la liste des (x, y) visités jusqu'à présent. Ensuite, vous comparez chacun (x2, y2) à l et vous l'acceptez seulement s'il n'est pas dans l. Ensuite, vous l'ajoutez au nouveau l.
Adam Rosenfield
2
Je l'ai fait aussi quand je suis tombé malade de jouer à Scramble. Je pense que la solution récursive (DFS au lieu de BFS) est plus sexy, car vous pouvez simplement conserver un ensemble de cellules actives (donc vous ne visitez pas la même cellule deux fois). Beaucoup plus soigné que de garder un tas de listes.
Justin Scheiner
2
Cela ne devrait-il pas tomber dans une boucle infinie? Je veux dire, disons (x,y), un adepte possible est (x+1,y+1). Puis (x+1,y+1)est poussé dans la file d'attente. Mais (x,y)sera également un adepte (x+1,y+1), donc cela ne conduira-t-il pas à un rebond sans fin entre eux?
SexyBeast
39

Pour une accélération de dictionnaire, il y a une transformation / processus général que vous pouvez faire pour réduire considérablement les comparaisons de dictionnaire à l'avance.

Étant donné que la grille ci-dessus ne contient que 16 caractères, certains d'entre eux en double, vous pouvez réduire considérablement le nombre de clés totales dans votre dictionnaire en filtrant simplement les entrées qui ont des caractères inaccessibles.

Je pensais que c'était l'optimisation évidente mais vu que personne ne l'a fait, je le mentionne.

Cela m'a réduit d'un dictionnaire de 200 000 clés à seulement 2 000 clés simplement pendant la passe d'entrée. Cela réduit à tout le moins la surcharge de la mémoire, et c'est sûr de correspondre à une augmentation de vitesse quelque part car la mémoire n'est pas infiniment rapide.

Implémentation de Perl

Mon implémentation est un peu lourde car j'ai accordé de l'importance à pouvoir connaître le chemin exact de chaque chaîne extraite, pas seulement la validité qui s'y trouve.

J'ai également quelques adaptations qui permettraient théoriquement à une grille avec des trous de fonctionner, et des grilles avec des lignes de tailles différentes (en supposant que vous obtenez la bonne entrée et qu'elle s'aligne d'une manière ou d'une autre).

Le filtre précoce est de loin le goulot d' étranglement le plus important dans mon application, comme on le soupçonnait plus tôt, commentant que la ligne le gonfle de 1,5 s à 7,5 s.

Lors de l'exécution, il semble penser que tous les chiffres sont sur leurs propres mots valides, mais je suis presque sûr que c'est dû au fonctionnement du fichier dictionnaire.

C'est un peu gonflé, mais au moins je réutilise Tree :: Trie de cpan

Certains d'entre eux ont été inspirés en partie par les implémentations existantes, certains j'avais déjà en tête.

La critique constructive et les façons de l'améliorer sont les bienvenues (/ moi note qu'il n'a jamais recherché CPAN pour un résolveur de boggles , mais c'était plus amusant à travailler)

mis à jour pour de nouveaux critères

#!/usr/bin/perl 

use strict;
use warnings;

{

  # this package manages a given path through the grid.
  # Its an array of matrix-nodes in-order with
  # Convenience functions for pretty-printing the paths
  # and for extending paths as new paths.

  # Usage:
  # my $p = Prefix->new(path=>[ $startnode ]);
  # my $c = $p->child( $extensionNode );
  # print $c->current_word ;

  package Prefix;
  use Moose;

  has path => (
      isa     => 'ArrayRef[MatrixNode]',
      is      => 'rw',
      default => sub { [] },
  );
  has current_word => (
      isa        => 'Str',
      is         => 'rw',
      lazy_build => 1,
  );

  # Create a clone of this object
  # with a longer path

  # $o->child( $successive-node-on-graph );

  sub child {
      my $self    = shift;
      my $newNode = shift;
      my $f       = Prefix->new();

      # Have to do this manually or other recorded paths get modified
      push @{ $f->{path} }, @{ $self->{path} }, $newNode;
      return $f;
  }

  # Traverses $o->path left-to-right to get the string it represents.

  sub _build_current_word {
      my $self = shift;
      return join q{}, map { $_->{value} } @{ $self->{path} };
  }

  # Returns  the rightmost node on this path

  sub tail {
      my $self = shift;
      return $self->{path}->[-1];
  }

  # pretty-format $o->path

  sub pp_path {
      my $self = shift;
      my @path =
        map { '[' . $_->{x_position} . ',' . $_->{y_position} . ']' }
        @{ $self->{path} };
      return "[" . join( ",", @path ) . "]";
  }

  # pretty-format $o
  sub pp {
      my $self = shift;
      return $self->current_word . ' => ' . $self->pp_path;
  }

  __PACKAGE__->meta->make_immutable;
}

{

  # Basic package for tracking node data
  # without having to look on the grid.
  # I could have just used an array or a hash, but that got ugly.

# Once the matrix is up and running it doesn't really care so much about rows/columns,
# Its just a sea of points and each point has adjacent points.
# Relative positioning is only really useful to map it back to userspace

  package MatrixNode;
  use Moose;

  has x_position => ( isa => 'Int', is => 'rw', required => 1 );
  has y_position => ( isa => 'Int', is => 'rw', required => 1 );
  has value      => ( isa => 'Str', is => 'rw', required => 1 );
  has siblings   => (
      isa     => 'ArrayRef[MatrixNode]',
      is      => 'rw',
      default => sub { [] }
  );

# Its not implicitly uni-directional joins. It would be more effient in therory
# to make the link go both ways at the same time, but thats too hard to program around.
# and besides, this isn't slow enough to bother caring about.

  sub add_sibling {
      my $self    = shift;
      my $sibling = shift;
      push @{ $self->siblings }, $sibling;
  }

  # Convenience method to derive a path starting at this node

  sub to_path {
      my $self = shift;
      return Prefix->new( path => [$self] );
  }
  __PACKAGE__->meta->make_immutable;

}

{

  package Matrix;
  use Moose;

  has rows => (
      isa     => 'ArrayRef',
      is      => 'rw',
      default => sub { [] },
  );

  has regex => (
      isa        => 'Regexp',
      is         => 'rw',
      lazy_build => 1,
  );

  has cells => (
      isa        => 'ArrayRef',
      is         => 'rw',
      lazy_build => 1,
  );

  sub add_row {
      my $self = shift;
      push @{ $self->rows }, [@_];
  }

  # Most of these functions from here down are just builder functions,
  # or utilities to help build things.
  # Some just broken out to make it easier for me to process.
  # All thats really useful is add_row
  # The rest will generally be computed, stored, and ready to go
  # from ->cells by the time either ->cells or ->regex are called.

  # traverse all cells and make a regex that covers them.
  sub _build_regex {
      my $self  = shift;
      my $chars = q{};
      for my $cell ( @{ $self->cells } ) {
          $chars .= $cell->value();
      }
      $chars = "[^$chars]";
      return qr/$chars/i;
  }

  # convert a plain cell ( ie: [x][y] = 0 )
  # to an intelligent cell ie: [x][y] = object( x, y )
  # we only really keep them in this format temporarily
  # so we can go through and tie in neighbouring information.
  # after the neigbouring is done, the grid should be considered inoperative.

  sub _convert {
      my $self = shift;
      my $x    = shift;
      my $y    = shift;
      my $v    = $self->_read( $x, $y );
      my $n    = MatrixNode->new(
          x_position => $x,
          y_position => $y,
          value      => $v,
      );
      $self->_write( $x, $y, $n );
      return $n;
  }

# go through the rows/collums presently available and freeze them into objects.

  sub _build_cells {
      my $self = shift;
      my @out  = ();
      my @rows = @{ $self->{rows} };
      for my $x ( 0 .. $#rows ) {
          next unless defined $self->{rows}->[$x];
          my @col = @{ $self->{rows}->[$x] };
          for my $y ( 0 .. $#col ) {
              next unless defined $self->{rows}->[$x]->[$y];
              push @out, $self->_convert( $x, $y );
          }
      }
      for my $c (@out) {
          for my $n ( $self->_neighbours( $c->x_position, $c->y_position ) ) {
              $c->add_sibling( $self->{rows}->[ $n->[0] ]->[ $n->[1] ] );
          }
      }
      return \@out;
  }

  # given x,y , return array of points that refer to valid neighbours.
  sub _neighbours {
      my $self = shift;
      my $x    = shift;
      my $y    = shift;
      my @out  = ();
      for my $sx ( -1, 0, 1 ) {
          next if $sx + $x < 0;
          next if not defined $self->{rows}->[ $sx + $x ];
          for my $sy ( -1, 0, 1 ) {
              next if $sx == 0 && $sy == 0;
              next if $sy + $y < 0;
              next if not defined $self->{rows}->[ $sx + $x ]->[ $sy + $y ];
              push @out, [ $sx + $x, $sy + $y ];
          }
      }
      return @out;
  }

  sub _has_row {
      my $self = shift;
      my $x    = shift;
      return defined $self->{rows}->[$x];
  }

  sub _has_cell {
      my $self = shift;
      my $x    = shift;
      my $y    = shift;
      return defined $self->{rows}->[$x]->[$y];
  }

  sub _read {
      my $self = shift;
      my $x    = shift;
      my $y    = shift;
      return $self->{rows}->[$x]->[$y];
  }

  sub _write {
      my $self = shift;
      my $x    = shift;
      my $y    = shift;
      my $v    = shift;
      $self->{rows}->[$x]->[$y] = $v;
      return $v;
  }

  __PACKAGE__->meta->make_immutable;
}

use Tree::Trie;

sub readDict {
  my $fn = shift;
  my $re = shift;
  my $d  = Tree::Trie->new();

  # Dictionary Loading
  open my $fh, '<', $fn;
  while ( my $line = <$fh> ) {
      chomp($line);

 # Commenting the next line makes it go from 1.5 seconds to 7.5 seconds. EPIC.
      next if $line =~ $re;    # Early Filter
      $d->add( uc($line) );
  }
  return $d;
}

sub traverseGraph {
  my $d     = shift;
  my $m     = shift;
  my $min   = shift;
  my $max   = shift;
  my @words = ();

  # Inject all grid nodes into the processing queue.

  my @queue =
    grep { $d->lookup( $_->current_word ) }
    map  { $_->to_path } @{ $m->cells };

  while (@queue) {
      my $item = shift @queue;

      # put the dictionary into "exact match" mode.

      $d->deepsearch('exact');

      my $cword = $item->current_word;
      my $l     = length($cword);

      if ( $l >= $min && $d->lookup($cword) ) {
          push @words,
            $item;    # push current path into "words" if it exactly matches.
      }
      next if $l > $max;

      # put the dictionary into "is-a-prefix" mode.
      $d->deepsearch('boolean');

    siblingloop: foreach my $sibling ( @{ $item->tail->siblings } ) {
          foreach my $visited ( @{ $item->{path} } ) {
              next siblingloop if $sibling == $visited;
          }

          # given path y , iterate for all its end points
          my $subpath = $item->child($sibling);

          # create a new path for each end-point
          if ( $d->lookup( $subpath->current_word ) ) {

             # if the new path is a prefix, add it to the bottom of the queue.
              push @queue, $subpath;
          }
      }
  }
  return \@words;
}

sub setup_predetermined { 
  my $m = shift; 
  my $gameNo = shift;
  if( $gameNo == 0 ){
      $m->add_row(qw( F X I E ));
      $m->add_row(qw( A M L O ));
      $m->add_row(qw( E W B X ));
      $m->add_row(qw( A S T U ));
      return $m;
  }
  if( $gameNo == 1 ){
      $m->add_row(qw( D G H I ));
      $m->add_row(qw( K L P S ));
      $m->add_row(qw( Y E U T ));
      $m->add_row(qw( E O R N ));
      return $m;
  }
}
sub setup_random { 
  my $m = shift; 
  my $seed = shift;
  srand $seed;
  my @letters = 'A' .. 'Z' ; 
  for( 1 .. 4 ){ 
      my @r = ();
      for( 1 .. 4 ){
          push @r , $letters[int(rand(25))];
      }
      $m->add_row( @r );
  }
}

# Here is where the real work starts.

my $m = Matrix->new();
setup_predetermined( $m, 0 );
#setup_random( $m, 5 );

my $d = readDict( 'dict.txt', $m->regex );
my $c = scalar @{ $m->cells };    # get the max, as per spec

print join ",\n", map { $_->pp } @{
  traverseGraph( $d, $m, 3, $c ) ;
};

Arch / information d'exécution pour comparaison:

model name      : Intel(R) Core(TM)2 Duo CPU     T9300  @ 2.50GHz
cache size      : 6144 KB
Memory usage summary: heap total: 77057577, heap peak: 11446200, stack peak: 26448
       total calls   total memory   failed calls
 malloc|     947212       68763684              0
realloc|      11191        1045641              0  (nomove:9063, dec:4731, free:0)
 calloc|     121001        7248252              0
   free|     973159       65854762

Histogram for block sizes:
  0-15         392633  36% ==================================================
 16-31          43530   4% =====
 32-47          50048   4% ======
 48-63          70701   6% =========
 64-79          18831   1% ==
 80-95          19271   1% ==
 96-111        238398  22% ==============================
112-127          3007  <1% 
128-143        236727  21% ==============================

Plus de marmonnements sur cette optimisation Regex

L'optimisation des expressions rationnelles que j'utilise est inutile pour les dictionnaires multi-résolutions, et pour les multi-résolutions, vous voudrez un dictionnaire complet, pas pré-découpé.

Cependant, cela dit, pour les résolutions ponctuelles, c'est vraiment rapide. (Les expressions rationnelles de Perl sont en C! :))

Voici quelques ajouts de code différents:

sub readDict_nofilter {
  my $fn = shift;
  my $re = shift;
  my $d  = Tree::Trie->new();

  # Dictionary Loading
  open my $fh, '<', $fn;
  while ( my $line = <$fh> ) {
      chomp($line);
      $d->add( uc($line) );
  }
  return $d;
}

sub benchmark_io { 
  use Benchmark qw( cmpthese :hireswallclock );
   # generate a random 16 character string 
   # to simulate there being an input grid. 
  my $regexen = sub { 
      my @letters = 'A' .. 'Z' ; 
      my @lo = ();
      for( 1..16 ){ 
          push @lo , $_ ; 
      }
      my $c  = join '', @lo;
      $c = "[^$c]";
      return qr/$c/i;
  };
  cmpthese( 200 , { 
      filtered => sub { 
          readDict('dict.txt', $regexen->() );
      }, 
      unfiltered => sub {
          readDict_nofilter('dict.txt');
      }
  });
}
           s / iter non filtré filtré
non filtré 8,16 - -94%
filtré 0,464 1658% -

ps: 8,16 * 200 = 27 minutes.

Kent Fredric
la source
2
Je sais que j'échoue au club d'optimisation, mais j'ai eu des problèmes de vitesse avant d'arriver au vrai travail du code, et réduire le temps d'entrée de 2 s à 1,2 s signifie beaucoup pour moi.
Kent Fredric
/ me l'a noté étrange maintenant, il a fallu moins de temps pour regex et ignorer les entrées que pour ajouter des clés à un hachage.
Kent Fredric
Nice, une implémentation Perl! Je vais l'exécuter maintenant.
Paolo Bergantino
Blerg, ayant du mal à installer Tree :: Trie sur mon serveur web. :(
Paolo Bergantino
3
Comment avez-vous généré ce dernier rapport (arch / info d'exécution)? Semble utile.
jmanning2k
33

Vous pouvez diviser le problème en deux:

  1. Une sorte d'algorithme de recherche qui énumérera les chaînes possibles dans la grille.
  2. Une façon de tester si une chaîne est un mot valide.

Idéalement, (2) devrait également inclure un moyen de tester si une chaîne est un préfixe d'un mot valide - cela vous permettra d'élaguer votre recherche et de gagner un tas de temps.

Trie d'Adam Rosenfield est une solution à (2). C'est élégant et probablement ce que votre spécialiste en algorithmes préférerait, mais avec les langages modernes et les ordinateurs modernes, nous pouvons être un peu plus paresseux. De plus, comme Kent le suggère, nous pouvons réduire la taille de notre dictionnaire en supprimant les mots dont les lettres ne sont pas présentes dans la grille. Voici du python:

def make_lookups(grid, fn='dict.txt'):
    # Make set of valid characters.
    chars = set()
    for word in grid:
        chars.update(word)

    words = set(x.strip() for x in open(fn) if set(x.strip()) <= chars)
    prefixes = set()
    for w in words:
        for i in range(len(w)+1):
            prefixes.add(w[:i])

    return words, prefixes

Sensationnel; test de préfixe à temps constant. Il faut quelques secondes pour charger le dictionnaire que vous avez lié, mais seulement quelques :-) (remarquez que words <= prefixes)

Maintenant, pour la partie (1), je suis enclin à penser en termes de graphiques. Je vais donc créer un dictionnaire qui ressemble à ceci:

graph = { (x, y):set([(x0,y0), (x1,y1), (x2,y2)]), }

c'est-à graph[(x, y)]- dire l'ensemble des coordonnées que vous pouvez atteindre à partir de la position (x, y). J'ajouterai également un nœud factice Nonequi se connectera à tout.

Construire, c'est un peu maladroit, car il y a 8 positions possibles et vous devez faire une vérification des limites. Voici un code python maladroit correspondant:

def make_graph(grid):
    root = None
    graph = { root:set() }
    chardict = { root:'' }

    for i, row in enumerate(grid):
        for j, char in enumerate(row):
            chardict[(i, j)] = char
            node = (i, j)
            children = set()
            graph[node] = children
            graph[root].add(node)
            add_children(node, children, grid)

    return graph, chardict

def add_children(node, children, grid):
    x0, y0 = node
    for i in [-1,0,1]:
        x = x0 + i
        if not (0 <= x < len(grid)):
            continue
        for j in [-1,0,1]:
            y = y0 + j
            if not (0 <= y < len(grid[0])) or (i == j == 0):
                continue

            children.add((x,y))

Ce code crée également un mappage de dictionnaire (x,y)avec le caractère correspondant. Cela me permet de transformer une liste de postes en un mot:

def to_word(chardict, pos_list):
    return ''.join(chardict[x] for x in pos_list)

Enfin, nous effectuons une recherche en profondeur. La procédure de base est la suivante:

  1. La recherche arrive à un nœud particulier.
  2. Vérifiez si le chemin jusqu'à présent peut faire partie d'un mot. Sinon, n'explorez pas cette branche plus loin.
  3. Vérifiez si le chemin jusqu'à présent est un mot. Si oui, ajoutez à la liste des résultats.
  4. Explorez tous les enfants qui ne font pas partie du chemin jusqu'à présent.

Python:

def find_words(graph, chardict, position, prefix, results, words, prefixes):
    """ Arguments:
      graph :: mapping (x,y) to set of reachable positions
      chardict :: mapping (x,y) to character
      position :: current position (x,y) -- equals prefix[-1]
      prefix :: list of positions in current string
      results :: set of words found
      words :: set of valid words in the dictionary
      prefixes :: set of valid words or prefixes thereof
    """
    word = to_word(chardict, prefix)

    if word not in prefixes:
        return

    if word in words:
        results.add(word)

    for child in graph[position]:
        if child not in prefix:
            find_words(graph, chardict, child, prefix+[child], results, words, prefixes)

Exécutez le code en tant que:

grid = ['fxie', 'amlo', 'ewbx', 'astu']
g, c = make_graph(grid)
w, p = make_lookups(grid)
res = set()
find_words(g, c, None, [], res, w, p)

et inspectez respour voir les réponses. Voici une liste de mots trouvés pour votre exemple, triés par taille:

 ['a', 'b', 'e', 'f', 'i', 'l', 'm', 'o', 's', 't',
 'u', 'w', 'x', 'ae', 'am', 'as', 'aw', 'ax', 'bo',
 'bu', 'ea', 'el', 'em', 'es', 'fa', 'ie', 'io', 'li',
 'lo', 'ma', 'me', 'mi', 'oe', 'ox', 'sa', 'se', 'st',
 'tu', 'ut', 'wa', 'we', 'xi', 'aes', 'ame', 'ami',
 'ase', 'ast', 'awa', 'awe', 'awl', 'blo', 'but', 'elb',
 'elm', 'fae', 'fam', 'lei', 'lie', 'lim', 'lob', 'lox',
 'mae', 'maw', 'mew', 'mil', 'mix', 'oil', 'olm', 'saw',
 'sea', 'sew', 'swa', 'tub', 'tux', 'twa', 'wae', 'was',
 'wax', 'wem', 'ambo', 'amil', 'amli', 'asem', 'axil',
 'axle', 'bleo', 'boil', 'bole', 'east', 'fame', 'limb',
 'lime', 'mesa', 'mewl', 'mile', 'milo', 'oime', 'sawt',
 'seam', 'seax', 'semi', 'stub', 'swam', 'twae', 'twas',
 'wame', 'wase', 'wast', 'weam', 'west', 'amble', 'awest',
 'axile', 'embox', 'limbo', 'limes', 'swami', 'embole',
 'famble', 'semble', 'wamble']

Le code prend (littéralement) quelques secondes pour charger le dictionnaire, mais le reste est instantané sur ma machine.

John Fouhy
la source
Très agréable! Très rapide aussi. Je vais attendre pour voir si quelqu'un d'autre monte dans l'assiette, mais votre réponse semble bonne jusqu'à présent.
Paolo Bergantino
Je ne comprends pas pourquoi "embole" est votre seul mot de 6 lettres, j'ai 10 mots différents pour cela. Il semble que vous interdisiez de visiter le même nœud deux fois, et comme l'OP l'a indiqué, c'est un jeu équitable.
Kent Fredric le
1
ok, il y a peut-être encore un bug car il rejette "FAMBLE" "WAMBLE" et "SEMBLE", qui ne partagent pas les caractères.
Kent Fredric
Bien repéré! Le bug était dans la création du jeu de préfixes: je devais utiliser à la range(len(w)+1)place de range(len(w)). Je l'ai affirmé words <= prefixesmais apparemment, je n'ai pas testé cela: - /
John Fouhy
1
Cela m'a aidé à apprendre comment fonctionne un DFS et comment l'implémenter. Je n'étais sûr d'aucun moyen de montrer sa gratitude pour cela autrement qu'avec un commentaire. Merci!
Graham Smith
23

Ma tentative en Java. Il faut environ 2 s pour lire le fichier et construire le trie, et environ 50 ms pour résoudre le puzzle. J'ai utilisé le dictionnaire lié dans la question (il a quelques mots que je ne connaissais pas en anglais comme fae, ima)

0 [main] INFO gineer.bogglesolver.util.Util  - Reading the dictionary
2234 [main] INFO gineer.bogglesolver.util.Util  - Finish reading the dictionary
2234 [main] INFO gineer.bogglesolver.Solver  - Found: FAM
2234 [main] INFO gineer.bogglesolver.Solver  - Found: FAME
2234 [main] INFO gineer.bogglesolver.Solver  - Found: FAMBLE
2234 [main] INFO gineer.bogglesolver.Solver  - Found: FAE
2234 [main] INFO gineer.bogglesolver.Solver  - Found: IMA
2234 [main] INFO gineer.bogglesolver.Solver  - Found: ELI
2234 [main] INFO gineer.bogglesolver.Solver  - Found: ELM
2234 [main] INFO gineer.bogglesolver.Solver  - Found: ELB
2234 [main] INFO gineer.bogglesolver.Solver  - Found: AXIL
2234 [main] INFO gineer.bogglesolver.Solver  - Found: AXILE
2234 [main] INFO gineer.bogglesolver.Solver  - Found: AXLE
2234 [main] INFO gineer.bogglesolver.Solver  - Found: AMI
2234 [main] INFO gineer.bogglesolver.Solver  - Found: AMIL
2234 [main] INFO gineer.bogglesolver.Solver  - Found: AMLI
2234 [main] INFO gineer.bogglesolver.Solver  - Found: AME
2234 [main] INFO gineer.bogglesolver.Solver  - Found: AMBLE
2234 [main] INFO gineer.bogglesolver.Solver  - Found: AMBO
2250 [main] INFO gineer.bogglesolver.Solver  - Found: AES
2250 [main] INFO gineer.bogglesolver.Solver  - Found: AWL
2250 [main] INFO gineer.bogglesolver.Solver  - Found: AWE
2250 [main] INFO gineer.bogglesolver.Solver  - Found: AWEST
2250 [main] INFO gineer.bogglesolver.Solver  - Found: AWA
2250 [main] INFO gineer.bogglesolver.Solver  - Found: MIX
2250 [main] INFO gineer.bogglesolver.Solver  - Found: MIL
2250 [main] INFO gineer.bogglesolver.Solver  - Found: MILE
2250 [main] INFO gineer.bogglesolver.Solver  - Found: MILO
2250 [main] INFO gineer.bogglesolver.Solver  - Found: MAX
2250 [main] INFO gineer.bogglesolver.Solver  - Found: MAE
2250 [main] INFO gineer.bogglesolver.Solver  - Found: MAW
2250 [main] INFO gineer.bogglesolver.Solver  - Found: MEW
2250 [main] INFO gineer.bogglesolver.Solver  - Found: MEWL
2250 [main] INFO gineer.bogglesolver.Solver  - Found: MES
2250 [main] INFO gineer.bogglesolver.Solver  - Found: MESA
2250 [main] INFO gineer.bogglesolver.Solver  - Found: MWA
2250 [main] INFO gineer.bogglesolver.Solver  - Found: MWA
2250 [main] INFO gineer.bogglesolver.Solver  - Found: LIE
2250 [main] INFO gineer.bogglesolver.Solver  - Found: LIM
2250 [main] INFO gineer.bogglesolver.Solver  - Found: LIMA
2250 [main] INFO gineer.bogglesolver.Solver  - Found: LIMAX
2250 [main] INFO gineer.bogglesolver.Solver  - Found: LIME
2250 [main] INFO gineer.bogglesolver.Solver  - Found: LIMES
2250 [main] INFO gineer.bogglesolver.Solver  - Found: LIMB
2250 [main] INFO gineer.bogglesolver.Solver  - Found: LIMBO
2250 [main] INFO gineer.bogglesolver.Solver  - Found: LIMBU
2250 [main] INFO gineer.bogglesolver.Solver  - Found: LEI
2250 [main] INFO gineer.bogglesolver.Solver  - Found: LEO
2250 [main] INFO gineer.bogglesolver.Solver  - Found: LOB
2250 [main] INFO gineer.bogglesolver.Solver  - Found: LOX
2250 [main] INFO gineer.bogglesolver.Solver  - Found: OIME
2250 [main] INFO gineer.bogglesolver.Solver  - Found: OIL
2250 [main] INFO gineer.bogglesolver.Solver  - Found: OLE
2250 [main] INFO gineer.bogglesolver.Solver  - Found: OLM
2250 [main] INFO gineer.bogglesolver.Solver  - Found: EMIL
2250 [main] INFO gineer.bogglesolver.Solver  - Found: EMBOLE
2250 [main] INFO gineer.bogglesolver.Solver  - Found: EMBOX
2250 [main] INFO gineer.bogglesolver.Solver  - Found: EAST
2250 [main] INFO gineer.bogglesolver.Solver  - Found: WAF
2250 [main] INFO gineer.bogglesolver.Solver  - Found: WAX
2250 [main] INFO gineer.bogglesolver.Solver  - Found: WAME
2250 [main] INFO gineer.bogglesolver.Solver  - Found: WAMBLE
2250 [main] INFO gineer.bogglesolver.Solver  - Found: WAE
2250 [main] INFO gineer.bogglesolver.Solver  - Found: WEA
2250 [main] INFO gineer.bogglesolver.Solver  - Found: WEAM
2250 [main] INFO gineer.bogglesolver.Solver  - Found: WEM
2250 [main] INFO gineer.bogglesolver.Solver  - Found: WEA
2250 [main] INFO gineer.bogglesolver.Solver  - Found: WES
2250 [main] INFO gineer.bogglesolver.Solver  - Found: WEST
2250 [main] INFO gineer.bogglesolver.Solver  - Found: WAE
2250 [main] INFO gineer.bogglesolver.Solver  - Found: WAS
2250 [main] INFO gineer.bogglesolver.Solver  - Found: WASE
2250 [main] INFO gineer.bogglesolver.Solver  - Found: WAST
2250 [main] INFO gineer.bogglesolver.Solver  - Found: BLEO
2250 [main] INFO gineer.bogglesolver.Solver  - Found: BLO
2250 [main] INFO gineer.bogglesolver.Solver  - Found: BOIL
2250 [main] INFO gineer.bogglesolver.Solver  - Found: BOLE
2250 [main] INFO gineer.bogglesolver.Solver  - Found: BUT
2250 [main] INFO gineer.bogglesolver.Solver  - Found: AES
2250 [main] INFO gineer.bogglesolver.Solver  - Found: AWA
2250 [main] INFO gineer.bogglesolver.Solver  - Found: AWL
2250 [main] INFO gineer.bogglesolver.Solver  - Found: AWE
2250 [main] INFO gineer.bogglesolver.Solver  - Found: AWEST
2250 [main] INFO gineer.bogglesolver.Solver  - Found: ASE
2250 [main] INFO gineer.bogglesolver.Solver  - Found: ASEM
2250 [main] INFO gineer.bogglesolver.Solver  - Found: AST
2250 [main] INFO gineer.bogglesolver.Solver  - Found: SEA
2250 [main] INFO gineer.bogglesolver.Solver  - Found: SEAX
2250 [main] INFO gineer.bogglesolver.Solver  - Found: SEAM
2250 [main] INFO gineer.bogglesolver.Solver  - Found: SEMI
2250 [main] INFO gineer.bogglesolver.Solver  - Found: SEMBLE
2250 [main] INFO gineer.bogglesolver.Solver  - Found: SEW
2250 [main] INFO gineer.bogglesolver.Solver  - Found: SEA
2250 [main] INFO gineer.bogglesolver.Solver  - Found: SWA
2250 [main] INFO gineer.bogglesolver.Solver  - Found: SWAM
2250 [main] INFO gineer.bogglesolver.Solver  - Found: SWAMI
2250 [main] INFO gineer.bogglesolver.Solver  - Found: SWA
2250 [main] INFO gineer.bogglesolver.Solver  - Found: SAW
2250 [main] INFO gineer.bogglesolver.Solver  - Found: SAWT
2250 [main] INFO gineer.bogglesolver.Solver  - Found: STU
2250 [main] INFO gineer.bogglesolver.Solver  - Found: STUB
2250 [main] INFO gineer.bogglesolver.Solver  - Found: TWA
2250 [main] INFO gineer.bogglesolver.Solver  - Found: TWAE
2250 [main] INFO gineer.bogglesolver.Solver  - Found: TWA
2250 [main] INFO gineer.bogglesolver.Solver  - Found: TWAE
2250 [main] INFO gineer.bogglesolver.Solver  - Found: TWAS
2250 [main] INFO gineer.bogglesolver.Solver  - Found: TUB
2250 [main] INFO gineer.bogglesolver.Solver  - Found: TUX

Le code source se compose de 6 classes. Je les posterai ci-dessous (si ce n'est pas la bonne pratique sur StackOverflow, dites-le moi).

gineer.bogglesolver.Main

package gineer.bogglesolver;

import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Logger;

public class Main
{
    private final static Logger logger = Logger.getLogger(Main.class);

    public static void main(String[] args)
    {
        BasicConfigurator.configure();

        Solver solver = new Solver(4,
                        "FXIE" +
                        "AMLO" +
                        "EWBX" +
                        "ASTU");
        solver.solve();

    }
}

gineer.bogglesolver.Solver

package gineer.bogglesolver;

import gineer.bogglesolver.trie.Trie;
import gineer.bogglesolver.util.Constants;
import gineer.bogglesolver.util.Util;
import org.apache.log4j.Logger;

public class Solver
{
    private char[] puzzle;
    private int maxSize;

    private boolean[] used;
    private StringBuilder stringSoFar;

    private boolean[][] matrix;
    private Trie trie;

    private final static Logger logger = Logger.getLogger(Solver.class);

    public Solver(int size, String puzzle)
    {
        trie = Util.getTrie(size);
        matrix = Util.connectivityMatrix(size);

        maxSize = size * size;
        stringSoFar = new StringBuilder(maxSize);
        used = new boolean[maxSize];

        if (puzzle.length() == maxSize)
        {
            this.puzzle = puzzle.toCharArray();
        }
        else
        {
            logger.error("The puzzle size does not match the size specified: " + puzzle.length());
            this.puzzle = puzzle.substring(0, maxSize).toCharArray();
        }
    }

    public void solve()
    {
        for (int i = 0; i < maxSize; i++)
        {
            traverseAt(i);
        }
    }

    private void traverseAt(int origin)
    {
        stringSoFar.append(puzzle[origin]);
        used[origin] = true;

        //Check if we have a valid word
        if ((stringSoFar.length() >= Constants.MINIMUM_WORD_LENGTH) && (trie.containKey(stringSoFar.toString())))
        {
            logger.info("Found: " + stringSoFar.toString());
        }

        //Find where to go next
        for (int destination = 0; destination < maxSize; destination++)
        {
            if (matrix[origin][destination] && !used[destination] && trie.containPrefix(stringSoFar.toString() + puzzle[destination]))
            {
                traverseAt(destination);
            }
        }

        used[origin] = false;
        stringSoFar.deleteCharAt(stringSoFar.length() - 1);
    }

}

gineer.bogglesolver.trie.Node

package gineer.bogglesolver.trie;

import gineer.bogglesolver.util.Constants;

class Node
{
    Node[] children;
    boolean isKey;

    public Node()
    {
        isKey = false;
        children = new Node[Constants.NUMBER_LETTERS_IN_ALPHABET];
    }

    public Node(boolean key)
    {
        isKey = key;
        children = new Node[Constants.NUMBER_LETTERS_IN_ALPHABET];
    }

    /**
     Method to insert a string to Node and its children

     @param key the string to insert (the string is assumed to be uppercase)
     @return true if the node or one of its children is changed, false otherwise
     */
    public boolean insert(String key)
    {
        //If the key is empty, this node is a key
        if (key.length() == 0)
        {
            if (isKey)
                return false;
            else
            {
                isKey = true;
                return true;
            }
        }
        else
        {//otherwise, insert in one of its child

            int childNodePosition = key.charAt(0) - Constants.LETTER_A;
            if (children[childNodePosition] == null)
            {
                children[childNodePosition] = new Node();
                children[childNodePosition].insert(key.substring(1));
                return true;
            }
            else
            {
                return children[childNodePosition].insert(key.substring(1));
            }
        }
    }

    /**
     Returns whether key is a valid prefix for certain key in this trie.
     For example: if key "hello" is in this trie, tests with all prefixes "hel", "hell", "hello" return true

     @param prefix the prefix to check
     @return true if the prefix is valid, false otherwise
     */
    public boolean containPrefix(String prefix)
    {
        //If the prefix is empty, return true
        if (prefix.length() == 0)
        {
            return true;
        }
        else
        {//otherwise, check in one of its child
            int childNodePosition = prefix.charAt(0) - Constants.LETTER_A;
            return children[childNodePosition] != null && children[childNodePosition].containPrefix(prefix.substring(1));
        }
    }

    /**
     Returns whether key is a valid key in this trie.
     For example: if key "hello" is in this trie, tests with all prefixes "hel", "hell" return false

     @param key the key to check
     @return true if the key is valid, false otherwise
     */
    public boolean containKey(String key)
    {
        //If the prefix is empty, return true
        if (key.length() == 0)
        {
            return isKey;
        }
        else
        {//otherwise, check in one of its child
            int childNodePosition = key.charAt(0) - Constants.LETTER_A;
            return children[childNodePosition] != null && children[childNodePosition].containKey(key.substring(1));
        }
    }

    public boolean isKey()
    {
        return isKey;
    }

    public void setKey(boolean key)
    {
        isKey = key;
    }
}

gineer.bogglesolver.trie.Trie

package gineer.bogglesolver.trie;

public class Trie
{
    Node root;

    public Trie()
    {
        this.root = new Node();
    }

    /**
     Method to insert a string to Node and its children

     @param key the string to insert (the string is assumed to be uppercase)
     @return true if the node or one of its children is changed, false otherwise
     */
    public boolean insert(String key)
    {
        return root.insert(key.toUpperCase());
    }

    /**
     Returns whether key is a valid prefix for certain key in this trie.
     For example: if key "hello" is in this trie, tests with all prefixes "hel", "hell", "hello" return true

     @param prefix the prefix to check
     @return true if the prefix is valid, false otherwise
     */
    public boolean containPrefix(String prefix)
    {
        return root.containPrefix(prefix.toUpperCase());
    }

    /**
     Returns whether key is a valid key in this trie.
     For example: if key "hello" is in this trie, tests with all prefixes "hel", "hell" return false

     @param key the key to check
     @return true if the key is valid, false otherwise
     */
    public boolean containKey(String key)
    {
        return root.containKey(key.toUpperCase());
    }


}

gineer.bogglesolver.util.Constants

package gineer.bogglesolver.util;

public class Constants
{

    public static final int NUMBER_LETTERS_IN_ALPHABET = 26;
    public static final char LETTER_A = 'A';
    public static final int MINIMUM_WORD_LENGTH = 3;
    public static final int DEFAULT_PUZZLE_SIZE = 4;
}

gineer.bogglesolver.util.Util

package gineer.bogglesolver.util;

import gineer.bogglesolver.trie.Trie;
import org.apache.log4j.Logger;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class Util
{
    private final static Logger logger = Logger.getLogger(Util.class);
    private static Trie trie;
    private static int size = Constants.DEFAULT_PUZZLE_SIZE;

    /**
     Returns the trie built from the dictionary.  The size is used to eliminate words that are too long.

     @param size the size of puzzle.  The maximum lenght of words in the returned trie is (size * size)
     @return the trie that can be used for puzzle of that size
     */
    public static Trie getTrie(int size)
    {
        if ((trie != null) && size == Util.size)
            return trie;

        trie = new Trie();
        Util.size = size;

        logger.info("Reading the dictionary");
        final File file = new File("dictionary.txt");
        try
        {
            Scanner scanner = new Scanner(file);
            final int maxSize = size * size;
            while (scanner.hasNext())
            {
                String line = scanner.nextLine().replaceAll("[^\\p{Alpha}]", "");

                if (line.length() <= maxSize)
                    trie.insert(line);
            }
        }
        catch (FileNotFoundException e)
        {
            logger.error("Cannot open file", e);
        }

        logger.info("Finish reading the dictionary");
        return trie;
    }

    static boolean[] connectivityRow(int x, int y, int size)
    {
        boolean[] squares = new boolean[size * size];
        for (int offsetX = -1; offsetX <= 1; offsetX++)
        {
            for (int offsetY = -1; offsetY <= 1; offsetY++)
            {
                final int calX = x + offsetX;
                final int calY = y + offsetY;
                if ((calX >= 0) && (calX < size) && (calY >= 0) && (calY < size))
                    squares[calY * size + calX] = true;
            }
        }

        squares[y * size + x] = false;//the current x, y is false

        return squares;
    }

    /**
     Returns the matrix of connectivity between two points.  Point i can go to point j iff matrix[i][j] is true
     Square (x, y) is equivalent to point (size * y + x).  For example, square (1,1) is point 5 in a puzzle of size 4

     @param size the size of the puzzle
     @return the connectivity matrix
     */
    public static boolean[][] connectivityMatrix(int size)
    {
        boolean[][] matrix = new boolean[size * size][];
        for (int x = 0; x < size; x++)
        {
            for (int y = 0; y < size; y++)
            {
                matrix[y * size + x] = connectivityRow(x, y, size);
            }
        }
        return matrix;
    }
}
gineer
la source
1
Je comparais ma sortie avec les sorties d'autres StackOverflowers, et il semble que les sorties d'Adam, John et rvarcher manquaient quelques mots. Par exemple, "Mwa" est dans le dictionnaire (ouais!), Mais il n'est pas renvoyé dans les sorties d'Adam, John et rvarcher. Il est retourné deux fois dans le lien PHP de Paolo.
gineer
1
J'ai essayé celui-ci en copiant le coller. Il dit "Reading ..." et "Finish reading ...", mais rien n'apparaît après cela. Aucune correspondance n'est affichée.
MikkoP
23

Je pense que vous passerez probablement la plupart de votre temps à essayer de faire correspondre des mots qui ne peuvent pas être créés par votre grille de lettres. Donc, la première chose que je ferais est d'essayer d'accélérer cette étape et cela devrait vous aider à faire la plupart du chemin.

Pour cela, je voudrais ré-exprimer la grille sous forme de tableau des "mouvements" possibles que vous indexez par la transition de lettre que vous regardez.

Commencez par attribuer à chaque lettre un numéro de tout votre alphabet (A = 0, B = 1, C = 2, ... et ainsi de suite).

Prenons cet exemple:

h b c d
e e g h
l l k l
m o f p

Et pour l'instant, utilisons l'alphabet des lettres que nous avons (généralement, vous voudrez probablement utiliser le même alphabet entier à chaque fois):

 b | c | d | e | f | g | h | k | l | m |  o |  p
---+---+---+---+---+---+---+---+---+---+----+----
 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11

Ensuite, vous créez un tableau booléen 2D qui indique si vous avez une certaine transition de lettre disponible:

     |  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

Parcourez maintenant votre liste de mots et convertissez les mots en transitions:

hello (6, 3, 8, 8, 10):
6 -> 3, 3 -> 8, 8 -> 8, 8 -> 10

Vérifiez ensuite si ces transitions sont autorisées en les recherchant dans votre table:

[6][ 3] : T
[3][ 8] : T
[8][ 8] : T
[8][10] : T

S'ils sont tous autorisés, il y a une chance que ce mot soit trouvé.

Par exemple, le mot "casque" peut être exclu lors de la 4ème transition (m à e: helMEt), car cette entrée dans votre tableau est fausse.

Et le mot hamster peut être exclu, car la première transition (h vers a) n'est pas autorisée (n'existe même pas dans votre table).

Maintenant, pour les quelques mots probablement que vous n'avez pas éliminés, essayez de les trouver dans la grille comme vous le faites maintenant ou comme suggéré dans certaines des autres réponses ici. Ceci afin d'éviter les faux positifs résultant de sauts entre des lettres identiques dans votre grille. Par exemple, le mot "aide" est autorisé par le tableau, mais pas par la grille.

Quelques autres conseils d'amélioration des performances sur cette idée:

  1. Au lieu d'utiliser un tableau 2D, utilisez un tableau 1D et calculez simplement l'index de la deuxième lettre vous-même. Donc, au lieu d'un tableau 12x12 comme ci-dessus, faites un tableau 1D de longueur 144. Si vous utilisez ensuite toujours le même alphabet (c'est-à-dire un tableau 26x26 = 676x1 pour l'alphabet anglais standard), même si toutes les lettres n'apparaissent pas dans votre grille , vous pouvez pré-calculer les indices dans ce tableau 1D que vous devez tester pour faire correspondre vos mots de dictionnaire. Par exemple, les indices de «bonjour» dans l'exemple ci-dessus seraient

    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
    
  2. Étendez l'idée à une table 3D (exprimée sous forme de tableau 1D), c'est-à-dire toutes les combinaisons de 3 lettres autorisées. De cette façon, vous pouvez éliminer encore plus de mots immédiatement et vous réduisez le nombre de recherches de tableau pour chaque mot de 1: Pour 'hello', vous n'avez besoin que de 3 recherches de tableau: hel, ell, llo. Soit dit en passant, il sera très rapide de construire ce tableau, car il n'y a que 400 mouvements de 3 lettres possibles dans votre grille.

  3. Pré-calculez les indices des mouvements dans votre grille que vous devez inclure dans votre table. Pour l'exemple ci-dessus, vous devez définir les entrées suivantes sur «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]
    .
    :
    
  4. Représentez également votre grille de jeu dans un tableau 1-D avec 16 entrées et faites pré-calculer la table en 3. contient les indices dans ce tableau.

Je suis sûr que si vous utilisez cette approche, vous pouvez exécuter votre code incroyablement rapidement, si vous avez le dictionnaire pré-calculé et déjà chargé en mémoire.

BTW: Une autre bonne chose à faire, si vous construisez un jeu, est d'exécuter ce genre de choses immédiatement en arrière-plan. Commencez à générer et à résoudre le premier jeu pendant que l'utilisateur regarde toujours l'écran de titre de votre application et met son doigt en position pour appuyer sur "Play". Ensuite, générez et résolvez le jeu suivant pendant que l'utilisateur joue le précédent. Cela devrait vous donner beaucoup de temps pour exécuter votre code.

(J'aime ce problème, donc je serai probablement tenté d'implémenter ma proposition en Java dans les prochains jours pour voir comment cela fonctionnerait réellement ... Je posterai le code ici une fois que je le ferai.)

MISE À JOUR:

Ok, j'ai eu un peu de temps aujourd'hui et j'ai implémenté cette idée 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();
  }
}

Voici quelques résultats:

Pour la grille de l'image postée dans la question d'origine (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

Pour les lettres affichées comme exemple dans la question d'origine (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

Pour la grille 5x5 suivante:

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

cela donne ceci:

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

Pour cela, j'ai utilisé la liste de mots du scrabble du tournoi TWL06 , car le lien dans la question d'origine ne fonctionne plus. Ce fichier fait 1,85 Mo, il est donc un peu plus court. Et la buildDictionaryfonction jette tous les mots de moins de 3 lettres.

Voici quelques observations sur les performances de ceci:

  • C'est environ 10 fois plus lent que les performances rapportées de l'implémentation OCaml de Victor Nicollet. Que cela soit dû à l'algorithme différent, au dictionnaire plus court qu'il a utilisé, au fait que son code soit compilé et que le mien s'exécute dans une machine virtuelle Java, ou aux performances de nos ordinateurs (le mien est un Intel Q6600 @ 2,4 MHz exécutant WinXP), Je ne sais pas. Mais c'est beaucoup plus rapide que les résultats des autres implémentations cités à la fin de la question d'origine. Donc, que cet algorithme soit supérieur au dictionnaire trie ou non, je ne sais pas pour l'instant.

  • La méthode du tableau utilisée dans checkWordTriplets()donne une très bonne approximation des réponses réelles. Seul 1 mot sur 3 à 5 réussi échouera checkWords()(voir le nombre de candidats par rapport au nombre de mots réels ci-dessus).

  • Quelque chose que vous ne pouvez pas voir ci-dessus: La checkWordTriplets()fonction prend environ 3,65 ms et est donc pleinement dominante dans le processus de recherche. La checkWords()fonction prend à peu près les 0,05-0,20 ms restantes.

  • Le temps d'exécution de la checkWordTriplets()fonction dépend linéairement de la taille du dictionnaire et est pratiquement indépendant de la taille de la carte!

  • Le temps d'exécution de checkWords()dépend de la taille du tableau et du nombre de mots non exclus par checkWordTriplets().

  • L' checkWords()implémentation ci-dessus est la première version la plus stupide que j'ai trouvée. Il n'est fondamentalement pas du tout optimisé. Mais par rapport à checkWordTriplets()cela, cela n'a pas d'importance pour la performance totale de l'application, donc je ne m'en suis pas inquiété. Mais , si la taille de la carte augmente, cette fonction deviendra de plus en plus lente et finira par devenir importante. Ensuite, il devrait également être optimisé.

  • Une bonne chose à propos de ce code est sa flexibilité:

    • Vous pouvez facilement changer la taille de la carte: mettez à jour la ligne 10 et le tableau de chaînes transmis à initializeBoard().
    • Il peut prendre en charge des alphabets plus grands / différents et peut gérer des choses comme le traitement de «Qu» comme une seule lettre sans aucun frais généraux de performance. Pour ce faire, il faudrait mettre à jour la ligne 9 et le couple d'endroits où les caractères sont convertis en nombres (actuellement simplement en soustrayant 65 de la valeur ASCII)

D'accord, mais je pense que ce post est maintenant assez long. Je peux certainement répondre à toutes vos questions, mais passons aux commentaires.

Markus A.
la source
Bonne réponse. J'aimerais voir votre implémentation en Java.
MikkoP
@MikkoP Done! :) A pris environ 3 heures et 220 lignes de code. Bon moyen de passer un après-midi. Faites-moi savoir si vous avez des questions sur le fonctionnement ... :)
Markus A.
Merci d'avoir publié le code! Je l'ai essayé avec mon propre dictionnaire après avoir ajouté les importations manquantes. J'obtiens une ArrayIndexOutOfBoundException sur la ligne ok = possibleTriplets[entry.triplets[t]];. hmm?
MikkoP
@MikkoP Ce code est actuellement écrit pour supposer que le dictionnaire ne contient que des lettres majuscules AZ. Le nœud est à la ligne 34: entry.letters[i] = (byte) letter - 65;il prend simplement la valeur ASCII et soustrait 65 ("A"). Si vous avez des trémas ou des lettres minuscules dans votre dictionnaire, cela donnera des valeurs supérieures à 31, qui ne sont pas prévues par le réglage de la taille de l'alphabet à la ligne 9. Pour prendre en charge d'autres lettres, vous devrez développer cette ligne pour les mapper dans la plage autorisée par la taille de l'alphabet.
Markus A.
1
@AlexanderN Vous comprenez probablement la logique correctement. J'ai fait une erreur en copiant la grille des lettres ... Désolé ... (corrigé)
Markus A.
19

Étonnamment, personne n'a tenté une version PHP de cela.

Il s'agit d'une version PHP fonctionnelle de la solution Python de John Fouhy's.

Bien que j'aie pris quelques pointeurs des réponses de tout le monde, c'est surtout copié de John.

$boggle = "fxie
           amlo
           ewbx
           astu";

$alphabet = str_split(str_replace(array("\n", " ", "\r"), "", strtolower($boggle)));
$rows = array_map('trim', explode("\n", $boggle));
$dictionary = file("C:/dict.txt");
$prefixes = array(''=>'');
$words = array();
$regex = '/[' . implode('', $alphabet) . ']{3,}$/S';
foreach($dictionary as $k=>$value) {
    $value = trim(strtolower($value));
    $length = strlen($value);
    if(preg_match($regex, $value)) {
        for($x = 0; $x < $length; $x++) {
            $letter = substr($value, 0, $x+1);
            if($letter == $value) {
                $words[$value] = 1;
            } else {
                $prefixes[$letter] = 1;
            }
        }
    }
}

$graph = array();
$chardict = array();
$positions = array();
$c = count($rows);
for($i = 0; $i < $c; $i++) {
    $l = strlen($rows[$i]);
    for($j = 0; $j < $l; $j++) {
        $chardict[$i.','.$j] = $rows[$i][$j];
        $children = array();
        $pos = array(-1,0,1);
        foreach($pos as $z) {
            $xCoord = $z + $i;
            if($xCoord < 0 || $xCoord >= count($rows)) {
                continue;
            }
            $len = strlen($rows[0]);
            foreach($pos as $w) {
                $yCoord = $j + $w;
                if(($yCoord < 0 || $yCoord >= $len) || ($z == 0 && $w == 0)) {
                    continue;
                }
                $children[] = array($xCoord, $yCoord);
            }
        }
        $graph['None'][] = array($i, $j);
        $graph[$i.','.$j] = $children;
    }
}

function to_word($chardict, $prefix) {
    $word = array();
    foreach($prefix as $v) {
        $word[] = $chardict[$v[0].','.$v[1]];
    }
    return implode("", $word);
}

function find_words($graph, $chardict, $position, $prefix, $prefixes, &$results, $words) {
    $word = to_word($chardict, $prefix);
    if(!isset($prefixes[$word])) return false;

    if(isset($words[$word])) {
        $results[] = $word;
    }

    foreach($graph[$position] as $child) {
        if(!in_array($child, $prefix)) {
            $newprefix = $prefix;
            $newprefix[] = $child;
            find_words($graph, $chardict, $child[0].','.$child[1], $newprefix, $prefixes, $results, $words);
        }
    }
}

$solution = array();
find_words($graph, $chardict, 'None', array(), $prefixes, $solution);
print_r($solution);

Voici un lien en direct si vous voulez l'essayer. Bien que cela prenne environ 2 secondes sur ma machine locale, cela prend environ 5 secondes sur mon serveur Web. Dans les deux cas, ce n'est pas très rapide. Pourtant, c'est assez hideux, donc je peux imaginer que le temps peut être considérablement réduit. Tout conseil sur la façon d'y parvenir serait apprécié. Le manque de tuples de PHP a rendu les coordonnées étranges avec lesquelles travailler et mon incapacité à comprendre exactement ce qui se passe n'a pas aidé du tout.

EDIT : Quelques corrections font qu'il faut moins de 1s localement.

Paolo Bergantino
la source
+1 @ "et mon incapacité à comprendre ce qui se passe n'a pas aidé du tout." lol. J'adore l'honnêteté!
dna123
Je ne connais pas PHP, mais la première chose que j'essaierais est de hisser '/ ['. imploser ('', $ alphabet). '] {3,} $ /' hors de la boucle. Autrement dit, définissez une variable sur celle-ci et utilisez la variable à la place dans la boucle.
Darius Bacon
Je suis presque sûr que PHP conserve un cache global par thread d'expressions régulières compilées, mais je vais essayer de toute façon.
Paolo Bergantino
1
@Daniel: Apparemment, c'est mon serveur Web. Cela ne se produit pas lorsque je cours localement. Hausser les épaules. Je n'ai pas vraiment envie de le traquer.
Paolo Bergantino
2
Qu'est-ce qui devrait être défini comme 7. paramètre dans la fonction find_words à la fin?
MikkoP
16

Pas intéressé par VB? :) Je n'ai pas pu résister. J'ai résolu cela différemment de la plupart des solutions présentées ici.

Mes horaires sont:

  • Chargement du dictionnaire et des préfixes de mots dans une table de hachage: 0,5 à 1 seconde.
  • Trouver les mots: moyenne sous 10 millisecondes.

EDIT: les temps de chargement du dictionnaire sur le serveur hôte Web s'exécutent environ 1 à 1,5 seconde de plus que mon ordinateur personnel.

Je ne sais pas à quel point les temps se détérioreront avec une charge sur le serveur.

J'ai écrit ma solution sous forme de page Web en .Net. myvrad.com/boggle

J'utilise le dictionnaire référencé dans la question d'origine.

Les lettres ne sont pas réutilisées dans un mot. Seuls les mots de 3 caractères ou plus sont trouvés.

J'utilise une table de hachage de tous les préfixes et mots de mots uniques au lieu d'un trie. Je ne connaissais pas les trios, alors j'ai appris quelque chose là-bas. L'idée de créer une liste de préfixes de mots en plus des mots complets est ce qui a finalement réduit mon temps à un nombre respectable.

Lisez les commentaires du code pour plus de détails.

Voici le code:

Imports System.Collections.Generic
Imports System.IO

Partial Class boggle_Default

    'Bob Archer, 4/15/2009

    'To avoid using a 2 dimensional array in VB I'm not using typical X,Y
    'coordinate iteration to find paths.
    '
    'I have locked the code into a 4 by 4 grid laid out like so:
    ' abcd
    ' efgh
    ' ijkl
    ' mnop
    ' 
    'To find paths the code starts with a letter from a to p then
    'explores the paths available around it. If a neighboring letter
    'already exists in the path then we don't go there.
    '
    'Neighboring letters (grid points) are hard coded into
    'a Generic.Dictionary below.



    'Paths is a list of only valid Paths found. 
    'If a word prefix or word is not found the path is not
    'added and extending that path is terminated.
    Dim Paths As New Generic.List(Of String)

    'NeighborsOf. The keys are the letters a to p.
    'The value is a string of letters representing neighboring letters.
    'The string of neighboring letters is split and iterated later.
    Dim NeigborsOf As New Generic.Dictionary(Of String, String)

    'BoggleLetters. The keys are mapped to the lettered grid of a to p.
    'The values are what the user inputs on the page.
    Dim BoggleLetters As New Generic.Dictionary(Of String, String)

    'Used to store last postition of path. This will be a letter
    'from a to p.
    Dim LastPositionOfPath As String = ""

    'I found a HashTable was by far faster than a Generic.Dictionary 
    ' - about 10 times faster. This stores prefixes of words and words.
    'I determined 792773 was the number of words and unique prefixes that
    'will be generated from the dictionary file. This is a max number and
    'the final hashtable will not have that many.
    Dim HashTableOfPrefixesAndWords As New Hashtable(792773)

    'Stores words that are found.
    Dim FoundWords As New Generic.List(Of String)

    'Just to validate what the user enters in the grid.
    Dim ErrorFoundWithSubmittedLetters As Boolean = False

    Public Sub BuildAndTestPathsAndFindWords(ByVal ThisPath As String)
        'Word is the word correlating to the ThisPath parameter.
        'This path would be a series of letters from a to p.
        Dim Word As String = ""

        'The path is iterated through and a word based on the actual
        'letters in the Boggle grid is assembled.
        For i As Integer = 0 To ThisPath.Length - 1
            Word += Me.BoggleLetters(ThisPath.Substring(i, 1))
        Next

        'If my hashtable of word prefixes and words doesn't contain this Word
        'Then this isn't a word and any further extension of ThisPath will not
        'yield any words either. So exit sub to terminate exploring this path.
        If Not HashTableOfPrefixesAndWords.ContainsKey(Word) Then Exit Sub

        'The value of my hashtable is a boolean representing if the key if a word (true) or
        'just a prefix (false). If true and at least 3 letters long then yay! word found.
        If HashTableOfPrefixesAndWords(Word) AndAlso Word.Length > 2 Then Me.FoundWords.Add(Word)

        'If my List of Paths doesn't contain ThisPath then add it.
        'Remember only valid paths will make it this far. Paths not found
        'in the HashTableOfPrefixesAndWords cause this sub to exit above.
        If Not Paths.Contains(ThisPath) Then Paths.Add(ThisPath)

        'Examine the last letter of ThisPath. We are looking to extend the path
        'to our neighboring letters if any are still available.
        LastPositionOfPath = ThisPath.Substring(ThisPath.Length - 1, 1)

        'Loop through my list of neighboring letters (representing grid points).
        For Each Neighbor As String In Me.NeigborsOf(LastPositionOfPath).ToCharArray()
            'If I find a neighboring grid point that I haven't already used
            'in ThisPath then extend ThisPath and feed the new path into
            'this recursive function. (see recursive.)
            If Not ThisPath.Contains(Neighbor) Then Me.BuildAndTestPathsAndFindWords(ThisPath & Neighbor)
        Next
    End Sub

    Protected Sub ButtonBoggle_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles ButtonBoggle.Click

        'User has entered the 16 letters and clicked the go button.

        'Set up my Generic.Dictionary of grid points, I'm using letters a to p -
        'not an x,y grid system.  The values are neighboring points.
        NeigborsOf.Add("a", "bfe")
        NeigborsOf.Add("b", "cgfea")
        NeigborsOf.Add("c", "dhgfb")
        NeigborsOf.Add("d", "hgc")
        NeigborsOf.Add("e", "abfji")
        NeigborsOf.Add("f", "abcgkjie")
        NeigborsOf.Add("g", "bcdhlkjf")
        NeigborsOf.Add("h", "cdlkg")
        NeigborsOf.Add("i", "efjnm")
        NeigborsOf.Add("j", "efgkonmi")
        NeigborsOf.Add("k", "fghlponj")
        NeigborsOf.Add("l", "ghpok")
        NeigborsOf.Add("m", "ijn")
        NeigborsOf.Add("n", "ijkom")
        NeigborsOf.Add("o", "jklpn")
        NeigborsOf.Add("p", "klo")

        'Retrieve letters the user entered.
        BoggleLetters.Add("a", Me.TextBox1.Text.ToLower.Trim())
        BoggleLetters.Add("b", Me.TextBox2.Text.ToLower.Trim())
        BoggleLetters.Add("c", Me.TextBox3.Text.ToLower.Trim())
        BoggleLetters.Add("d", Me.TextBox4.Text.ToLower.Trim())
        BoggleLetters.Add("e", Me.TextBox5.Text.ToLower.Trim())
        BoggleLetters.Add("f", Me.TextBox6.Text.ToLower.Trim())
        BoggleLetters.Add("g", Me.TextBox7.Text.ToLower.Trim())
        BoggleLetters.Add("h", Me.TextBox8.Text.ToLower.Trim())
        BoggleLetters.Add("i", Me.TextBox9.Text.ToLower.Trim())
        BoggleLetters.Add("j", Me.TextBox10.Text.ToLower.Trim())
        BoggleLetters.Add("k", Me.TextBox11.Text.ToLower.Trim())
        BoggleLetters.Add("l", Me.TextBox12.Text.ToLower.Trim())
        BoggleLetters.Add("m", Me.TextBox13.Text.ToLower.Trim())
        BoggleLetters.Add("n", Me.TextBox14.Text.ToLower.Trim())
        BoggleLetters.Add("o", Me.TextBox15.Text.ToLower.Trim())
        BoggleLetters.Add("p", Me.TextBox16.Text.ToLower.Trim())

        'Validate user entered something with a length of 1 for all 16 textboxes.
        For Each S As String In BoggleLetters.Keys
            If BoggleLetters(S).Length <> 1 Then
                ErrorFoundWithSubmittedLetters = True
                Exit For
            End If
        Next

        'If input is not valid then...
        If ErrorFoundWithSubmittedLetters Then
            'Present error message.
        Else
            'Else assume we have 16 letters to work with and start finding words.
            Dim SB As New StringBuilder

            Dim Time As String = String.Format("{0}:{1}:{2}:{3}", Date.Now.Hour.ToString(), Date.Now.Minute.ToString(), Date.Now.Second.ToString(), Date.Now.Millisecond.ToString())

            Dim NumOfLetters As Integer = 0
            Dim Word As String = ""
            Dim TempWord As String = ""
            Dim Letter As String = ""
            Dim fr As StreamReader = Nothing
            fr = New System.IO.StreamReader(HttpContext.Current.Request.MapPath("~/boggle/dic.txt"))

            'First fill my hashtable with word prefixes and words.
            'HashTable(PrefixOrWordString, BooleanTrueIfWordFalseIfPrefix)
            While fr.Peek <> -1
                Word = fr.ReadLine.Trim()
                TempWord = ""
                For i As Integer = 0 To Word.Length - 1
                    Letter = Word.Substring(i, 1)
                    'This optimization helped quite a bit. Words in the dictionary that begin
                    'with letters that the user did not enter in the grid shouldn't go in my hashtable.
                    '
                    'I realize most of the solutions went with a Trie. I'd never heard of that before,
                    'which is one of the neat things about SO, seeing how others approach challenges
                    'and learning some best practices.
                    '
                    'However, I didn't code a Trie in my solution. I just have a hashtable with 
                    'all words in the dicitonary file and all possible prefixes for those words.
                    'A Trie might be faster but I'm not coding it now. I'm getting good times with this.
                    If i = 0 AndAlso Not BoggleLetters.ContainsValue(Letter) Then Continue While
                    TempWord += Letter
                    If Not HashTableOfPrefixesAndWords.ContainsKey(TempWord) Then
                        HashTableOfPrefixesAndWords.Add(TempWord, TempWord = Word)
                    End If
                Next
            End While

            SB.Append("Number of Word Prefixes and Words in Hashtable: " & HashTableOfPrefixesAndWords.Count.ToString())
            SB.Append("<br />")

            SB.Append("Loading Dictionary: " & Time & " - " & String.Format("{0}:{1}:{2}:{3}", Date.Now.Hour.ToString(), Date.Now.Minute.ToString(), Date.Now.Second.ToString(), Date.Now.Millisecond.ToString()))
            SB.Append("<br />")

            Time = String.Format("{0}:{1}:{2}:{3}", Date.Now.Hour.ToString(), Date.Now.Minute.ToString(), Date.Now.Second.ToString(), Date.Now.Millisecond.ToString())

            'This starts a path at each point on the grid an builds a path until 
            'the string of letters correlating to the path is not found in the hashtable
            'of word prefixes and words.
            Me.BuildAndTestPathsAndFindWords("a")
            Me.BuildAndTestPathsAndFindWords("b")
            Me.BuildAndTestPathsAndFindWords("c")
            Me.BuildAndTestPathsAndFindWords("d")
            Me.BuildAndTestPathsAndFindWords("e")
            Me.BuildAndTestPathsAndFindWords("f")
            Me.BuildAndTestPathsAndFindWords("g")
            Me.BuildAndTestPathsAndFindWords("h")
            Me.BuildAndTestPathsAndFindWords("i")
            Me.BuildAndTestPathsAndFindWords("j")
            Me.BuildAndTestPathsAndFindWords("k")
            Me.BuildAndTestPathsAndFindWords("l")
            Me.BuildAndTestPathsAndFindWords("m")
            Me.BuildAndTestPathsAndFindWords("n")
            Me.BuildAndTestPathsAndFindWords("o")
            Me.BuildAndTestPathsAndFindWords("p")

            SB.Append("Finding Words: " & Time & " - " & String.Format("{0}:{1}:{2}:{3}", Date.Now.Hour.ToString(), Date.Now.Minute.ToString(), Date.Now.Second.ToString(), Date.Now.Millisecond.ToString()))
            SB.Append("<br />")

            SB.Append("Num of words found: " & FoundWords.Count.ToString())
            SB.Append("<br />")
            SB.Append("<br />")

            FoundWords.Sort()
            SB.Append(String.Join("<br />", FoundWords.ToArray()))

            'Output results.
            Me.LiteralBoggleResults.Text = SB.ToString()
            Me.PanelBoggleResults.Visible = True

        End If

    End Sub

End Class
rvarcher
la source
Je vais supposer ici que vous avez utilisé le système ap au lieu de [x] [y] car ce dernier est plutôt complexe en VB? J'ai passé une journée à essayer d'obtenir un tableau dynamique à 2 voies dans cette fois, c'est-à-dire: tableau (tableau (1, "bonjour"), 1, "bonjour", tableau ()), je ne sais toujours pas comment faire que: P
Kent Fredric
En PHP et Perl 2, les tableaux dim sont amusants. Cela peut être fait en VB mais je n'appellerais pas cela un processus amusant. Dim Arr (,) As Integer = {{1,1}, {0,0}}. Le processus AP est né de moi, me mettant sur la grille et demandant: «Où puis-je aller d'ici? Je sais que c'est une solution rigide mais cela fonctionne ici.
rvarcher
Ohh j'aime VB.NET ... J'ai essayé l'URL mais ça n'a pas marché. J'ai dû reconstruire moi-même votre code en tant que Windows Forms et cela fonctionne. Merci.
Ahmed Eissa
11

Dès que j'ai vu l'énoncé du problème, j'ai pensé "Trie". Mais vu que plusieurs autres affiches utilisaient cette approche, j'ai cherché une autre approche juste pour être différente. Hélas, l'approche Trie est plus performante. J'ai exécuté la solution Perl de Kent sur ma machine et il a fallu 0,31 seconde pour l'exécuter, après l'avoir adaptée pour utiliser mon fichier de dictionnaire. Ma propre implémentation perl a nécessité 0,54 seconde pour s'exécuter.

C'était mon approche:

  1. Créez un hachage de transition pour modéliser les transitions légales.

  2. Parcourez les 16 ^ 3 combinaisons possibles de trois lettres.

    • Dans la boucle, excluez les transitions illégales et les visites répétées sur la même place. Formez toutes les séquences légales de 3 lettres et stockez-les dans un hachage.
  3. Parcourez ensuite tous les mots du dictionnaire.

    • Exclure les mots trop longs ou trop courts
    • Faites glisser une fenêtre de 3 lettres sur chaque mot et voyez s'il fait partie des combinaisons de 3 lettres de l'étape 2. Excluez les mots qui échouent. Cela élimine la plupart des non-correspondances.
    • S'il n'est toujours pas éliminé, utilisez un algorithme récursif pour voir si le mot peut être formé en faisant des chemins à travers le puzzle. (Cette partie est lente, mais appelée rarement.)
  4. Imprimez les mots que j'ai trouvés.

    J'ai essayé des séquences de 3 et 4 lettres, mais des séquences de 4 lettres ont ralenti le programme.

Dans mon code, j'utilise / usr / share / dict / words pour mon dictionnaire. Il est livré en standard sur MAC OS X et de nombreux systèmes Unix. Vous pouvez utiliser un autre fichier si vous le souhaitez. Pour résoudre un autre puzzle, changez simplement la variable @puzzle. Ce serait facile à adapter pour des matrices plus grandes. Il vous suffirait de modifier le hachage% transitions et le hachage% legalTransitions.

La force de cette solution est que le code est court et les structures de données simples.

Voici le code Perl (qui utilise trop de variables globales, je sais):

#!/usr/bin/perl
use Time::HiRes  qw{ time };

sub readFile($);
sub findAllPrefixes($);
sub isWordTraceable($);
sub findWordsInPuzzle(@);

my $startTime = time;

# Puzzle to solve

my @puzzle = ( 
    F, X, I, E,
    A, M, L, O,
    E, W, B, X,
    A, S, T, U
);

my $minimumWordLength = 3;
my $maximumPrefixLength = 3; # I tried four and it slowed down.

# Slurp the word list.
my $wordlistFile = "/usr/share/dict/words";

my @words = split(/\n/, uc(readFile($wordlistFile)));
print "Words loaded from word list: " . scalar @words . "\n";

print "Word file load time: " . (time - $startTime) . "\n";
my $postLoad = time;

# Define the legal transitions from one letter position to another. 
# Positions are numbered 0-15.
#     0  1  2  3
#     4  5  6  7
#     8  9 10 11
#    12 13 14 15
my %transitions = ( 
   -1 => [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],
    0 => [1,4,5], 
    1 => [0,2,4,5,6],
    2 => [1,3,5,6,7],
    3 => [2,6,7],
    4 => [0,1,5,8,9],
    5 => [0,1,2,4,6,8,9,10],
    6 => [1,2,3,5,7,9,10,11],
    7 => [2,3,6,10,11],
    8 => [4,5,9,12,13],
    9 => [4,5,6,8,10,12,13,14],
    10 => [5,6,7,9,11,13,14,15],
    11 => [6,7,10,14,15],
    12 => [8,9,13],
    13 => [8,9,10,12,14],
    14 => [9,10,11,13,15],
    15 => [10,11,14]
);

# Convert the transition matrix into a hash for easy access.
my %legalTransitions = ();
foreach my $start (keys %transitions) {
    my $legalRef = $transitions{$start};
    foreach my $stop (@$legalRef) {
        my $index = ($start + 1) * (scalar @puzzle) + ($stop + 1);
        $legalTransitions{$index} = 1;
    }
}

my %prefixesInPuzzle = findAllPrefixes($maximumPrefixLength);

print "Find prefixes time: " . (time - $postLoad) . "\n";
my $postPrefix = time;

my @wordsFoundInPuzzle = findWordsInPuzzle(@words);

print "Find words in puzzle time: " . (time - $postPrefix) . "\n";

print "Unique prefixes found: " . (scalar keys %prefixesInPuzzle) . "\n";
print "Words found (" . (scalar @wordsFoundInPuzzle) . ") :\n    " . join("\n    ", @wordsFoundInPuzzle) . "\n";

print "Total Elapsed time: " . (time - $startTime) . "\n";

###########################################

sub readFile($) {
    my ($filename) = @_;
    my $contents;
    if (-e $filename) {
        # This is magic: it opens and reads a file into a scalar in one line of code. 
        # See http://www.perl.com/pub/a/2003/11/21/slurp.html
        $contents = do { local( @ARGV, $/ ) = $filename ; <> } ; 
    }
    else {
        $contents = '';
    }
    return $contents;
}

# Is it legal to move from the first position to the second? They must be adjacent.
sub isLegalTransition($$) {
    my ($pos1,$pos2) = @_;
    my $index = ($pos1 + 1) * (scalar @puzzle) + ($pos2 + 1);
    return $legalTransitions{$index};
}

# Find all prefixes where $minimumWordLength <= length <= $maxPrefixLength
#
#   $maxPrefixLength ... Maximum length of prefix we will store. Three gives best performance. 
sub findAllPrefixes($) {
    my ($maxPrefixLength) = @_;
    my %prefixes = ();
    my $puzzleSize = scalar @puzzle;

    # Every possible N-letter combination of the letters in the puzzle 
    # can be represented as an integer, though many of those combinations
    # involve illegal transitions, duplicated letters, etc.
    # Iterate through all those possibilities and eliminate the illegal ones.
    my $maxIndex = $puzzleSize ** $maxPrefixLength;

    for (my $i = 0; $i < $maxIndex; $i++) {
        my @path;
        my $remainder = $i;
        my $prevPosition = -1;
        my $prefix = '';
        my %usedPositions = ();
        for (my $prefixLength = 1; $prefixLength <= $maxPrefixLength; $prefixLength++) {
            my $position = $remainder % $puzzleSize;

            # Is this a valid step?
            #  a. Is the transition legal (to an adjacent square)?
            if (! isLegalTransition($prevPosition, $position)) {
                last;
            }

            #  b. Have we repeated a square?
            if ($usedPositions{$position}) {
                last;
            }
            else {
                $usedPositions{$position} = 1;
            }

            # Record this prefix if length >= $minimumWordLength.
            $prefix .= $puzzle[$position];
            if ($prefixLength >= $minimumWordLength) {
                $prefixes{$prefix} = 1;
            }

            push @path, $position;
            $remainder -= $position;
            $remainder /= $puzzleSize;
            $prevPosition = $position;
        } # end inner for
    } # end outer for
    return %prefixes;
}

# Loop through all words in dictionary, looking for ones that are in the puzzle.
sub findWordsInPuzzle(@) {
    my @allWords = @_;
    my @wordsFound = ();
    my $puzzleSize = scalar @puzzle;
WORD: foreach my $word (@allWords) {
        my $wordLength = length($word);
        if ($wordLength > $puzzleSize || $wordLength < $minimumWordLength) {
            # Reject word as too short or too long.
        }
        elsif ($wordLength <= $maximumPrefixLength ) {
            # Word should be in the prefix hash.
            if ($prefixesInPuzzle{$word}) {
                push @wordsFound, $word;
            }
        }
        else {
            # Scan through the word using a window of length $maximumPrefixLength, looking for any strings not in our prefix list.
            # If any are found that are not in the list, this word is not possible.
            # If no non-matches are found, we have more work to do.
            my $limit = $wordLength - $maximumPrefixLength + 1;
            for (my $startIndex = 0; $startIndex < $limit; $startIndex ++) {
                if (! $prefixesInPuzzle{substr($word, $startIndex, $maximumPrefixLength)}) {
                    next WORD;
                }
            }
            if (isWordTraceable($word)) {
                # Additional test necessary: see if we can form this word by following legal transitions
                push @wordsFound, $word;
            }
        }

    }
    return @wordsFound;
}

# Is it possible to trace out the word using only legal transitions?
sub isWordTraceable($) {
    my $word = shift;
    return traverse([split(//, $word)], [-1]); # Start at special square -1, which may transition to any square in the puzzle.
}

# Recursively look for a path through the puzzle that matches the word.
sub traverse($$) {
    my ($lettersRef, $pathRef) = @_;
    my $index = scalar @$pathRef - 1;
    my $position = $pathRef->[$index];
    my $letter = $lettersRef->[$index];
    my $branchesRef =  $transitions{$position};
BRANCH: foreach my $branch (@$branchesRef) {
            if ($puzzle[$branch] eq $letter) {
                # Have we used this position yet?
                foreach my $usedBranch (@$pathRef) {
                    if ($usedBranch == $branch) {
                        next BRANCH;
                    }
                }
                if (scalar @$lettersRef == $index + 1) {
                    return 1; # End of word and success.
                }
                push @$pathRef, $branch;
                if (traverse($lettersRef, $pathRef)) {
                    return 1; # Recursive success.
                }
                else {
                    pop @$pathRef;
                }
            }
        }
    return 0; # No path found. Failed.
}
Paul Chernoch
la source
L'emplacement du dictionnaire a-t-il changé? J'ai essayé de trouver les mots du dictionnaire car je voulais comparer ma solution avec tout le monde mais je ne l'ai pas trouvée sur le lien donné à / usr / share / dict. Je sais que c'est un fil assez ancien, mais ce serait bien si vous pouvez me pointer du doigt. Merci d'avance pour votre aide.
Naman
Je n'ai pas mon Mac à portée de main pour le moment. Tout ce dont vous avez besoin est un fichier avec des mots anglais, un à une ligne, séparés par des retours à la ligne. Vous pouvez trouver un tel fichier sur Internet. L'un est ici: mieliestronk.com/corncob_lowercase.txt mais il y a probablement des listes avec plus de mots que cela.
Paul Chernoch
Merci beaucoup pour la réponse. J'avais trouvé ça dans des fichiers ubuntu.
Naman
9

Je sais que je suis super en retard, mais j'en ai fait un il y a quelque temps en PHP - juste pour le plaisir aussi ...

http://www.lostsockdesign.com.au/sandbox/boggle/index.php?letters=fxieamloewbxastu 75 mots (133 pts) trouvés en 0.90108 secondes.

F.........X..I..............E............... A......................................M..............................L............................O............................... E....................W............................B..........................X A..................S..................................................T.................U....

Donne une indication de ce que le programme fait réellement - chaque lettre est l'endroit où elle commence à parcourir les modèles tandis que chaque «. montre un chemin qu'il a essayé de prendre. Le plus '.' il y en a plus qu'il a cherché.

Faites-moi savoir si vous voulez le code ... c'est un horrible mélange de PHP et HTML qui n'a jamais été conçu pour voir le jour donc je n'ose pas le poster ici: P

Danny
la source
9

J'ai passé 3 mois à travailler sur une solution au problème des 10 meilleures cartes Boggle denses 5x5.

Le problème est maintenant résolu et présenté avec une divulgation complète sur 5 pages Web. Veuillez me contacter avec des questions.

L'algorithme d'analyse de la carte utilise une pile explicite pour parcourir de manière pseudo-récursive les carrés de la carte via un graphique de mots acyclique dirigé avec des informations directes sur l'enfant et un mécanisme de suivi de l'horodatage. Il s'agit très probablement de la structure de données de lexique la plus avancée au monde.

Le schéma évalue quelque 10 000 très bonnes cartes par seconde sur un quad core. (9500+ points)

Page Web parent:

DeepSearch.c - http://www.pathcom.com/~vadco/deep.html

Pages Web des composants:

Tableau de bord optimal - http://www.pathcom.com/~vadco/binary.html

Structure avancée du lexique - http://www.pathcom.com/~vadco/adtdawg.html

Algorithme d'analyse de carte - http://www.pathcom.com/~vadco/guns.html

Traitement par lots parallèle - http://www.pathcom.com/~vadco/parallel.html

- Cet ensemble complet de travaux n'intéressera qu'une personne qui exige le meilleur.

JohnPaul Adamovsky
la source
4
Votre analyse est intéressante, mais vos résultats ne sont pas techniquement des tableaux Boggle. Le jeu de boggle 5x5 comprend un dé qui contient les visages BJKQXZ, votre implémentation exclut explicitement toutes ces lettres et donc la position du plateau n'est pas réellement possible dans un vrai jeu Boggle.
MarkPflug
4

Votre algorithme de recherche diminue-t-il continuellement la liste de mots à mesure que votre recherche se poursuit?

Par exemple, dans la recherche ci-dessus, il n'y a que 13 lettres par lesquelles vos mots peuvent commencer (ce qui réduit de moitié le nombre de lettres de départ).

Plus vous permutez de lettres, plus les ensembles de mots disponibles diminueront, ce qui réduira la recherche nécessaire.

Je commencerais par là.

jerebear
la source
4

Je devrais réfléchir davantage à une solution complète, mais comme une optimisation pratique, je me demande si cela pourrait valoir la peine de pré-calculer un tableau de fréquences de digrammes et de trigrammes (combinaisons de 2 et 3 lettres) basé sur tous les mots de votre dictionnaire, et utilisez-le pour prioriser votre recherche. J'irais avec les premières lettres de mots. Donc, si votre dictionnaire contient les mots "Inde", "Eau", "Extrême" et "Extraordinaire", votre tableau pré-calculé pourrait être:

'IN': 1
'WA': 1
'EX': 2

Recherchez ensuite ces digrammes dans l'ordre de similitude (d'abord EX, puis WA / IN)

Smashery
la source
4

Tout d'abord, lisez comment l'un des concepteurs du langage C # a résolu un problème connexe: http://blogs.msdn.com/ericlippert/archive/2009/02/04/a-nasality-talisman-for-the-sultana-analyst.aspx .

Comme lui, vous pouvez commencer avec un dictionnaire et canoniser les mots en créant un dictionnaire à partir d'un tableau de lettres triées alphabétiquement à une liste de mots qui peuvent être épelés à partir de ces lettres.

Ensuite, commencez à créer les mots possibles à partir du tableau et à les rechercher. Je suppose que cela vous mènera assez loin, mais il y a certainement plus de trucs qui pourraient accélérer les choses.

RossFabricant
la source
4

Je suggère de faire un arbre de lettres basé sur des mots. L'arbre serait composé d'une structure de lettre, comme ceci:

letter: char
isWord: boolean

Ensuite, vous construisez l'arbre, chaque profondeur ajoutant une nouvelle lettre. En d'autres termes, au premier niveau, il y aurait l'alphabet; puis de chacun de ces arbres, il y aurait encore 26 autres entrées, et ainsi de suite, jusqu'à ce que vous ayez épelé tous les mots. Accrochez-vous à cet arbre analysé, et cela rendra toutes les réponses possibles plus rapides à rechercher.

Avec cet arbre analysé, vous pouvez très rapidement trouver des solutions. Voici le pseudo-code:

BEGIN: 
    For each letter:
        if the struct representing it on the current depth has isWord == true, enter it as an answer.
        Cycle through all its neighbors; if there is a child of the current node corresponding to the letter, recursively call BEGIN on it.

Cela pourrait être accéléré avec un peu de programmation dynamique. Par exemple, dans votre échantillon, les deux «A» sont à la fois à côté d'un «E» et d'un «W», qui (à partir du moment où ils les ont frappés) seraient identiques. Je n'ai pas assez de temps pour vraiment énoncer le code pour cela, mais je pense que vous pouvez rassembler l'idée.

De plus, je suis sûr que vous trouverez d'autres solutions si vous recherchez Google pour "Boggle Solver".

Dan Lew
la source
3

Hilarant. J'ai presque posté la même question il y a quelques jours à cause du même putain de jeu! Je ne l'ai pas fait cependant, car j'ai simplement recherché sur Google pour python boggle solver et obtenu toutes les réponses que je pouvais souhaiter.

physiquemichael
la source
Je ne savais pas que le nom populaire était "boggle", mais j'ai trouvé des trucs sur google, j'étais simplement curieux de voir ce que les gens trouveraient sur SO. :)
Paolo Bergantino
3

Je me rends compte que l'heure de cette question est venue et révolue, mais comme je travaillais moi-même sur un solveur et que je suis tombé dessus en cherchant sur Google, j'ai pensé que je devrais poster une référence à la mienne car elle semble un peu différente de certaines autres.

J'ai choisi d'aller avec un tableau plat pour le plateau de jeu et de faire des chasses récursives à partir de chaque lettre du plateau, en passant d'un voisin valide à un voisin valide, en étendant la chasse si la liste actuelle des lettres est un préfixe valide dans un index. En parcourant la notion du mot courant, il y a la liste des index dans le tableau, pas les lettres qui composent un mot. Lors de la vérification de l'index, les index sont traduits en lettres et la vérification est effectuée.

L'index est un dictionnaire de force brute qui ressemble un peu à un trie, mais permet des requêtes pythoniques de l'index. Si les mots «chat» et «traiteur» sont dans la liste, vous obtiendrez ceci dans le dictionnaire:

   d = { 'c': ['cat','cater'],
     'ca': ['cat','cater'],
     'cat': ['cat','cater'],
     'cate': ['cater'],
     'cater': ['cater'],
   }

Donc, si le mot_courant est 'ca', vous savez que c'est un préfixe valide car 'ca' in drenvoie True (continuez donc la traversée de la carte). Et si le mot_actuel est 'cat', alors vous savez que c'est un mot valide car c'est un préfixe valide et 'cat' in d['cat']renvoie aussi True.

Si vous sentez que cela permet un code lisible qui ne semble pas trop lent. Comme tout le monde, la dépense dans ce système est de lire / construire l'index. Résoudre la planche est à peu près du bruit.

Le code se trouve à http://gist.github.com/268079 . Il est intentionnellement vertical et naïf avec beaucoup de vérification de validité explicite parce que je voulais comprendre le problème sans le rogner avec un tas de magie ou d'obscurité.

cdent
la source
3

J'ai écrit mon solveur en C ++. J'ai implémenté une arborescence personnalisée. Je ne suis pas sûr qu'il puisse être considéré comme un trie, mais c'est similaire. Chaque nœud a 26 branches, 1 pour chaque lettre de l'alphabet. Je traverse les branches de la planche à roulettes en parallèle avec les branches de mon dictionnaire. Si la branche n'existe pas dans le dictionnaire, j'arrête de la rechercher sur le tableau Boggle. Je convertis toutes les lettres du tableau en pouces. Donc 'A' = 0. Comme ce ne sont que des tableaux, la recherche est toujours O (1). Chaque nœud stocke s'il termine un mot et combien de mots existent dans ses enfants. L'arbre est taillé car les mots sont trouvés pour réduire la recherche répétée des mêmes mots. Je pense que l'élagage est également O (1).

Processeur: Pentium SU2700 1,3 GHz
RAM: 3 Go

Charge un dictionnaire de 178 590 mots en <1 seconde.
Résout le Boggle 100x100 (boggle.txt) en 4 secondes. ~ 44 000 mots trouvés.
Résoudre un Boggle 4x4 est trop rapide pour fournir une référence significative. :)

Résolution rapide Boggle Solver GitHub

Will
la source
2

Étant donné un tableau Boggle avec N lignes et M colonnes, supposons ce qui suit:

  • N * M est nettement supérieur au nombre de mots possibles
  • N * M est nettement supérieur au mot le plus long possible

Sous ces hypothèses, la complexité de cette solution est O (N * M).

Je pense que la comparaison des durées de fonctionnement de cet exemple de carte à bien des égards manque le point mais, par souci d'exhaustivité, cette solution se termine en <0,2 s sur mon MacBook Pro moderne.

Cette solution trouvera tous les chemins possibles pour chaque mot du corpus.

#!/usr/bin/env ruby
# Example usage: ./boggle-solver --board "fxie amlo ewbx astu"

autoload :Matrix, 'matrix'
autoload :OptionParser, 'optparse'

DEFAULT_CORPUS_PATH = '/usr/share/dict/words'.freeze

# Functions

def filter_corpus(matrix, corpus, min_word_length)
  board_char_counts = Hash.new(0)
  matrix.each { |c| board_char_counts[c] += 1 }

  max_word_length = matrix.row_count * matrix.column_count
  boggleable_regex = /^[#{board_char_counts.keys.reduce(:+)}]{#{min_word_length},#{max_word_length}}$/
  corpus.select{ |w| w.match boggleable_regex }.select do |w|
    word_char_counts = Hash.new(0)
    w.each_char { |c| word_char_counts[c] += 1 }
    word_char_counts.all? { |c, count| board_char_counts[c] >= count }
  end
end

def neighbors(point, matrix)
  i, j = point
  ([i-1, 0].max .. [i+1, matrix.row_count-1].min).inject([]) do |r, new_i|
    ([j-1, 0].max .. [j+1, matrix.column_count-1].min).inject(r) do |r, new_j|
      neighbor = [new_i, new_j]
      neighbor.eql?(point) ? r : r << neighbor
    end
  end
end

def expand_path(path, word, matrix)
  return [path] if path.length == word.length

  next_char = word[path.length]
  viable_neighbors = neighbors(path[-1], matrix).select do |point|
    !path.include?(point) && matrix.element(*point).eql?(next_char)
  end

  viable_neighbors.inject([]) do |result, point|
    result + expand_path(path.dup << point, word, matrix)
  end
end

def find_paths(word, matrix)
  result = []
  matrix.each_with_index do |c, i, j|
    result += expand_path([[i, j]], word, matrix) if c.eql?(word[0])
  end
  result
end

def solve(matrix, corpus, min_word_length: 3)
  boggleable_corpus = filter_corpus(matrix, corpus, min_word_length)
  boggleable_corpus.inject({}) do |result, w|
    paths = find_paths(w, matrix)
    result[w] = paths unless paths.empty?
    result
  end
end

# Script

options = { corpus_path: DEFAULT_CORPUS_PATH }
option_parser = OptionParser.new do |opts|
  opts.banner = 'Usage: boggle-solver --board <value> [--corpus <value>]'

  opts.on('--board BOARD', String, 'The board (e.g. "fxi aml ewb ast")') do |b|
    options[:board] = b
  end

  opts.on('--corpus CORPUS_PATH', String, 'Corpus file path') do |c|
    options[:corpus_path] = c
  end

  opts.on_tail('-h', '--help', 'Shows usage') do
    STDOUT.puts opts
    exit
  end
end
option_parser.parse!

unless options[:board]
  STDERR.puts option_parser
  exit false
end

unless File.file? options[:corpus_path]
  STDERR.puts "No corpus exists - #{options[:corpus_path]}"
  exit false
end

rows = options[:board].downcase.scan(/\S+/).map{ |row| row.scan(/./) }

raw_corpus = File.readlines(options[:corpus_path])
corpus = raw_corpus.map{ |w| w.downcase.rstrip }.uniq.sort

solution = solve(Matrix.rows(rows), corpus)
solution.each_pair do |w, paths|
  STDOUT.puts w
  paths.each do |path|
    STDOUT.puts "\t" + path.map{ |point| point.inspect }.join(', ')
  end
end
STDOUT.puts "TOTAL: #{solution.count}"
mon4goos
la source
2

Cette solution donne également la direction de recherche dans le tableau donné

Algo:

1. Uses trie to save all the word in the english to fasten the search
2. The uses DFS to search the words in Boggle

Production:

Found "pic" directions from (4,0)(p) go  → →
Found "pick" directions from (4,0)(p) go  → → ↑
Found "pickman" directions from (4,0)(p) go  → → ↑ ↑ ↖ ↑
Found "picket" directions from (4,0)(p) go  → → ↑ ↗ ↖
Found "picked" directions from (4,0)(p) go  → → ↑ ↗ ↘
Found "pickle" directions from (4,0)(p) go  → → ↑ ↘ →

Code:

from collections import defaultdict
from nltk.corpus import words
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize

english_words = words.words()

# If you wan to remove stop words
# stop_words = set(stopwords.words('english'))
# english_words = [w for w in english_words if w not in stop_words]

boggle = [
    ['c', 'n', 't', 's', 's'],
    ['d', 'a', 't', 'i', 'n'],
    ['o', 'o', 'm', 'e', 'l'],
    ['s', 'i', 'k', 'n', 'd'],
    ['p', 'i', 'c', 'l', 'e']
]

# Instead of X and Y co-ordinates
# better to use Row and column
lenc = len(boggle[0])
lenr = len(boggle)

# Initialize trie datastructure
trie_node = {'valid': False, 'next': {}}

# lets get the delta to find all the nighbors
neighbors_delta = [
    (-1,-1, "↖"),
    (-1, 0, "↑"),
    (-1, 1, "↗"),
    (0, -1, "←"),
    (0,  1, "→"),
    (1, -1, "↙"),
    (1,  0, "↓"),
    (1,  1, "↘"),
]


def gen_trie(word, node):
    """udpates the trie datastructure using the given word"""
    if not word:
        return

    if word[0] not in node:
        node[word[0]] = {'valid': len(word) == 1, 'next': {}}

    # recursively build trie
    gen_trie(word[1:], node[word[0]])


def build_trie(words, trie):
    """Builds trie data structure from the list of words given"""
    for word in words:
        gen_trie(word, trie)
    return trie


def get_neighbors(r, c):
    """Returns the neighbors for a given co-ordinates"""
    n = []
    for neigh in neighbors_delta:
        new_r = r + neigh[0]
        new_c = c + neigh[1]

        if (new_r >= lenr) or (new_c >= lenc) or (new_r < 0) or (new_c < 0):
            continue
        n.append((new_r, new_c, neigh[2]))
    return n


def dfs(r, c, visited, trie, now_word, direction):
    """Scan the graph using DFS"""
    if (r, c) in visited:
        return

    letter = boggle[r][c]
    visited.append((r, c))

    if letter in trie:
        now_word += letter

        if trie[letter]['valid']:
            print('Found "{}" {}'.format(now_word, direction))

        neighbors = get_neighbors(r, c)
        for n in neighbors:
            dfs(n[0], n[1], visited[::], trie[letter], now_word, direction + " " + n[2])


def main(trie_node):
    """Initiate the search for words in boggle"""
    trie_node = build_trie(english_words, trie_node)

    # print the board
    print("Given board")
    for i in range(lenr):print (boggle[i])
    print ('\n')

    for r in range(lenr):
        for c in range(lenc):
            letter = boggle[r][c]
            dfs(r, c, [], trie_node, '', 'directions from ({},{})({}) go '.format(r, c, letter))


if __name__ == '__main__':
    main(trie_node)
naren
la source
1

J'ai implémenté une solution dans OCaml . Il précompile un dictionnaire sous forme de trie et utilise des fréquences de séquence de deux lettres pour éliminer les bords qui ne pourraient jamais apparaître dans un mot pour accélérer encore le traitement.

Il résout votre exemple de carte en 0,35 ms (avec un temps de démarrage supplémentaire de 6 ms qui est principalement lié au chargement du trie en mémoire).

Les solutions trouvées:

["swami"; "emile"; "limbs"; "limbo"; "limes"; "amble"; "tubs"; "stub";
 "swam"; "semi"; "seam"; "awes"; "buts"; "bole"; "boil"; "west"; "east";
 "emil"; "lobs"; "limb"; "lime"; "lima"; "mesa"; "mews"; "mewl"; "maws";
 "milo"; "mile"; "awes"; "amie"; "axle"; "elma"; "fame"; "ubs"; "tux"; "tub";
 "twa"; "twa"; "stu"; "saw"; "sea"; "sew"; "sea"; "awe"; "awl"; "but"; "btu";
 "box"; "bmw"; "was"; "wax"; "oil"; "lox"; "lob"; "leo"; "lei"; "lie"; "mes";
 "mew"; "mae"; "maw"; "max"; "mil"; "mix"; "awe"; "awl"; "elm"; "eli"; "fax"]
Victor Nicollet
la source
C'est bien, mais toutes les heures publiées ici impliquent un temps de "démarrage" pour charger le dictionnaire en mémoire, donc comparer la 0.35 aux autres heures est assez loin d'être précis. De plus, utilisez-vous un dictionnaire différent? Vous manquez quelques mots. Quoi qu'il en soit, +1
Paolo Bergantino
Le temps de démarrage prend 6 ms, vous recherchez donc 6,35 ms pour une exécution complète. J'utilise mon /usr/share/dictdictionnaire local et certains mots sont effectivement manquants (comme EMBOLE).
Victor Nicollet
1

Une solution JavaScript Node.JS. Calcule les 100 mots uniques en moins d'une seconde, ce qui comprend la lecture d'un fichier de dictionnaire (MBA 2012).

Production:
["FAM", "TUX", "TUB", "FAE", "ELI", "ELM", "ELB", "TWA", "TWA", "SAW", "AMI", "SWA" , "SWA", "AME", "SEA", "SEW", "AES", "AWL", "AWE", "SEA", "AWA", "MIX", "MIL", "AST", " ASE "," MAX "," MAE "," MAW "," MEW "," AWE "," MES "," AWL "," LIE "," LIM "," AWA "," AES "," BUT " , "BLO", "WAS", "WAE", "WEA", "LEI", "LEO", "LOB", "LOX", "WEM", "OIL", "OLM", "WEA", " WAE "," WAX "," WAF ","MILO", "EAST", "WAME", "TWAS", "TWAE", "EMIL", "WEAM", "OIME", "AXIL", "WEST", "TWAE", "LIMB", "WASE "," WAST "," BLEO "," STUB "," BOIL "," BOLE "," LIME "," SAWT "," LIMA "," MESA "," MEWL "," AXLE "," FAME ", "ASEM", "MILE", "AMIL", "SEAX", "SEAM", "SEMI", "SWAM", "AMBO", "AMLI", "AXILE", "AMBLE", "SWAMI", "AWEST "," AWEST "," LIMAX "," LIMES "," LIMBU "," LIMBO "," EMBOX "," SEMBLE "," EMBOLE "," WAMBLE "," FAMBLE "]EST "," WAME "," TWAS "," TWAE "," EMIL "," WEAM "," OIME "," AXIL "," WEST "," TWAE "," LIMB "," WASE "," WAST " , "BLEO", "STUB", "BOIL", "BOLE", "LIME", "SAWT", "LIMA", "MESA", "MEWL", "AXLE", "FAME", "ASEM", " MILE "," AMIL "," SEAX "," SEAM "," SEMI "," SWAM "," AMBO "," AMLI "," AXILE "," AMBLE "," SWAMI "," AWEST "," AWEST " , "LIMAX", "LIMES", "LIMBU", "LIMBO", "EMBOX", "SEMBLE", "EMBOLE", "WAMBLE", "FAMBLE"]EST "," WAME "," TWAS "," TWAE "," EMIL "," WEAM "," OIME "," AXIL "," WEST "," TWAE "," LIMB "," WASE "," WAST " , "BLEO", "STUB", "BOIL", "BOLE", "LIME", "SAWT", "LIMA", "MESA", "MEWL", "AXLE", "FAME", "ASEM", " MILE "," AMIL "," SEAX "," SEAM "," SEMI "," SWAM "," AMBO "," AMLI "," AXILE "," AMBLE "," SWAMI "," AWEST "," AWEST " , "LIMAX", "LIMES", "LIMBU", "LIMBO", "EMBOX", "SEMBLE", "EMBOLE", "WAMBLE", "FAMBLE"]"TWAE", "EMIL", "WEAM", "OIME", "AXIL", "WEST", "TWAE", "LIMB", "WASE", "WAST", "BLEO", "STUB", "BOIL "," BOLE "," LIME "," SAWT "," LIMA "," MESA "," MEWL "," AXLE "," FAME "," ASEM "," MILE "," AMIL "," SEAX ", "SEAM", "SEMI", "SWAM", "AMBO", "AMLI", "AXILE", "AMBLE", "SWAMI", "AWEST", "AWEST", "LIMAX", "LIMES", "LIMBU "," LIMBO "," EMBOX "," SEMBLE "," EMBOLE "," WAMBLE "," FAMBLE "]"TWAE", "EMIL", "WEAM", "OIME", "AXIL", "WEST", "TWAE", "LIMB", "WASE", "WAST", "BLEO", "STUB", "BOIL "," BOLE "," LIME "," SAWT "," LIMA "," MESA "," MEWL "," AXLE "," FAME "," ASEM "," MILE "," AMIL "," SEAX ", "SEAM", "SEMI", "SWAM", "AMBO", "AMLI", "AXILE", "AMBLE", "SWAMI", "AWEST", "AWEST", "LIMAX", "LIMES", "LIMBU "," LIMBO "," EMBOX "," SEMBLE "," EMBOLE "," WAMBLE "," FAMBLE "]"WEST", "TWAE", "LIMB", "WASE", "WAST", "BLEO", "STUB", "BOIL", "BOLE", "LIME", "SAWT", "LIMA", "MESA "," MEWL "," AXLE "," FAME "," ASEM "," MILE "," AMIL "," SEAX "," SEAM "," SEMI "," SWAM "," AMBO "," AMLI ", "AXILE", "AMBLE", "SWAMI", "AWEST", "AWEST", "LIMAX", "LIMES", "LIMBU", "LIMBO", "EMBOX", "SEMBLE", "EMBOLE", "WAMBLE "," FAMBLE "]"WEST", "TWAE", "LIMB", "WASE", "WAST", "BLEO", "STUB", "BOIL", "BOLE", "LIME", "SAWT", "LIMA", "MESA "," MEWL "," AXLE "," FAME "," ASEM "," MILE "," AMIL "," SEAX "," SEAM "," SEMI "," SWAM "," AMBO "," AMLI ", "AXILE", "AMBLE", "SWAMI", "AWEST", "AWEST", "LIMAX", "LIMES", "LIMBU", "LIMBO", "EMBOX", "SEMBLE", "EMBOLE", "WAMBLE "," FAMBLE "]SAWT "," LIMA "," MESA "," MEWL "," AXLE "," FAME "," ASEM "," MILE "," AMIL "," SEAX "," SEAM "," SEMI "," SWAM " , "AMBO", "AMLI", "AXILE", "AMBLE", "SWAMI", "AWEST", "AWEST", "LIMAX", "LIMES", "LIMBU", "LIMBO", "EMBOX", " SEMBLE "," EMBOLE "," WAMBLE "," FAMBLE "]SAWT "," LIMA "," MESA "," MEWL "," AXLE "," FAME "," ASEM "," MILE "," AMIL "," SEAX "," SEAM "," SEMI "," SWAM " , "AMBO", "AMLI", "AXILE", "AMBLE", "SWAMI", "AWEST", "AWEST", "LIMAX", "LIMES", "LIMBU", "LIMBO", "EMBOX", " SEMBLE "," EMBOLE "," WAMBLE "," FAMBLE "]LIMAX "," LIMES "," LIMBU "," LIMBO "," EMBOX "," SEMBLE "," EMBOLE "," WAMBLE "," FAMBLE "]LIMAX "," LIMES "," LIMBU "," LIMBO "," EMBOX "," SEMBLE "," EMBOLE "," WAMBLE "," FAMBLE "]

Code:

var fs = require('fs')

var Node = function(value, row, col) {
    this.value = value
    this.row = row
    this.col = col
}

var Path = function() {
    this.nodes = []
}

Path.prototype.push = function(node) {
    this.nodes.push(node)
    return this
}

Path.prototype.contains = function(node) {
    for (var i = 0, ii = this.nodes.length; i < ii; i++) {
        if (this.nodes[i] === node) {
            return true
        }
    }

    return false
}

Path.prototype.clone = function() {
    var path = new Path()
    path.nodes = this.nodes.slice(0)
    return path
}

Path.prototype.to_word = function() {
    var word = ''

    for (var i = 0, ii = this.nodes.length; i < ii; ++i) {
        word += this.nodes[i].value
    }

    return word
}

var Board = function(nodes, dict) {
    // Expects n x m array.
    this.nodes = nodes
    this.words = []
    this.row_count = nodes.length
    this.col_count = nodes[0].length
    this.dict = dict
}

Board.from_raw = function(board, dict) {
    var ROW_COUNT = board.length
      , COL_COUNT = board[0].length

    var nodes = []

    // Replace board with Nodes
    for (var i = 0, ii = ROW_COUNT; i < ii; ++i) {
        nodes.push([])
        for (var j = 0, jj = COL_COUNT; j < jj; ++j) {
            nodes[i].push(new Node(board[i][j], i, j))
        }
    }

    return new Board(nodes, dict)
}

Board.prototype.toString = function() {
    return JSON.stringify(this.nodes)
}

Board.prototype.update_potential_words = function(dict) {
    for (var i = 0, ii = this.row_count; i < ii; ++i) {
        for (var j = 0, jj = this.col_count; j < jj; ++j) {
            var node = this.nodes[i][j]
              , path = new Path()

            path.push(node)

            this.dfs_search(path)
        }
    }
}

Board.prototype.on_board = function(row, col) {
    return 0 <= row && row < this.row_count && 0 <= col && col < this.col_count
}

Board.prototype.get_unsearched_neighbours = function(path) {
    var last_node = path.nodes[path.nodes.length - 1]

    var offsets = [
        [-1, -1], [-1,  0], [-1, +1]
      , [ 0, -1],           [ 0, +1]
      , [+1, -1], [+1,  0], [+1, +1]
    ]

    var neighbours = []

    for (var i = 0, ii = offsets.length; i < ii; ++i) {
        var offset = offsets[i]
        if (this.on_board(last_node.row + offset[0], last_node.col + offset[1])) {

            var potential_node = this.nodes[last_node.row + offset[0]][last_node.col + offset[1]]
            if (!path.contains(potential_node)) {
                // Create a new path if on board and we haven't visited this node yet.
                neighbours.push(potential_node)
            }
        }
    }

    return neighbours
}

Board.prototype.dfs_search = function(path) {
    var path_word = path.to_word()

    if (this.dict.contains_exact(path_word) && path_word.length >= 3) {
        this.words.push(path_word)
    }

    var neighbours = this.get_unsearched_neighbours(path)

    for (var i = 0, ii = neighbours.length; i < ii; ++i) {
        var neighbour = neighbours[i]
        var new_path = path.clone()
        new_path.push(neighbour)

        if (this.dict.contains_prefix(new_path.to_word())) {
            this.dfs_search(new_path)
        }
    }
}

var Dict = function() {
    this.dict_array = []

    var dict_data = fs.readFileSync('./web2', 'utf8')
    var dict_array = dict_data.split('\n')

    for (var i = 0, ii = dict_array.length; i < ii; ++i) {
        dict_array[i] = dict_array[i].toUpperCase()
    }

    this.dict_array = dict_array.sort()
}

Dict.prototype.contains_prefix = function(prefix) {
    // Binary search
    return this.search_prefix(prefix, 0, this.dict_array.length)
}

Dict.prototype.contains_exact = function(exact) {
    // Binary search
    return this.search_exact(exact, 0, this.dict_array.length)
}

Dict.prototype.search_prefix = function(prefix, start, end) {
    if (start >= end) {
        // If no more place to search, return no matter what.
        return this.dict_array[start].indexOf(prefix) > -1
    }

    var middle = Math.floor((start + end)/2)

    if (this.dict_array[middle].indexOf(prefix) > -1) {
        // If we prefix exists, return true.
        return true
    } else {
        // Recurse
        if (prefix <= this.dict_array[middle]) {
            return this.search_prefix(prefix, start, middle - 1)
        } else {
            return this.search_prefix(prefix, middle + 1, end)
        }
    }
}

Dict.prototype.search_exact = function(exact, start, end) {
    if (start >= end) {
        // If no more place to search, return no matter what.
        return this.dict_array[start] === exact
    }

    var middle = Math.floor((start + end)/2)

    if (this.dict_array[middle] === exact) {
        // If we prefix exists, return true.
        return true
    } else {
        // Recurse
        if (exact <= this.dict_array[middle]) {
            return this.search_exact(exact, start, middle - 1)
        } else {
            return this.search_exact(exact, middle + 1, end)
        }
    }
}

var board = [
    ['F', 'X', 'I', 'E']
  , ['A', 'M', 'L', 'O']
  , ['E', 'W', 'B', 'X']
  , ['A', 'S', 'T', 'U']
]

var dict = new Dict()

var b = Board.from_raw(board, dict)
b.update_potential_words()
console.log(JSON.stringify(b.words.sort(function(a, b) {
    return a.length - b.length
})))
Charlie Liang Yuan
la source
1

Je voulais donc ajouter une autre façon de résoudre ce problème avec PHP, car tout le monde aime PHP. Il y a un peu de refactorisation que je voudrais faire, comme utiliser une correspondance d'expression rationnelle avec le fichier de dictionnaire, mais en ce moment je charge juste le fichier de dictionnaire entier dans une wordList.

Je l'ai fait en utilisant une idée de liste chaînée. Chaque nœud a une valeur de caractère, une valeur d'emplacement et un pointeur suivant.

La valeur d'emplacement est la façon dont j'ai découvert si deux nœuds sont connectés.

1     2     3     4
11    12    13    14
21    22    23    24
31    32    33    34

Donc, en utilisant cette grille, je sais que deux nœuds sont connectés si l'emplacement du premier nœud est égal à l'emplacement du deuxième nœud +/- 1 pour la même ligne, +/- 9, 10, 11 pour la ligne au-dessus et en dessous.

J'utilise la récursivité pour la recherche principale. Il enlève un mot de la liste de mots, trouve tous les points de départ possibles, puis trouve récursivement la prochaine connexion possible, en gardant à l'esprit qu'il ne peut pas aller à un emplacement qu'il utilise déjà (c'est pourquoi j'ajoute $ notInLoc).

Quoi qu'il en soit, je sais qu'il a besoin d'être refactorisé et j'aimerais entendre des réflexions sur la façon de le rendre plus propre, mais il produit les bons résultats en fonction du fichier de dictionnaire que j'utilise. En fonction du nombre de voyelles et de combinaisons sur la carte, cela prend environ 3 à 6 secondes. Je sais qu'une fois que j'ai preg_match les résultats du dictionnaire, cela réduira considérablement.

<?php
    ini_set('xdebug.var_display_max_depth', 20);
    ini_set('xdebug.var_display_max_children', 1024);
    ini_set('xdebug.var_display_max_data', 1024);

    class Node {
        var $loc;

        function __construct($value) {
            $this->value = $value;
            $next = null;
        }
    }

    class Boggle {
        var $root;
        var $locList = array (1, 2, 3, 4, 11, 12, 13, 14, 21, 22, 23, 24, 31, 32, 33, 34);
        var $wordList = [];
        var $foundWords = [];

        function __construct($board) {
            // Takes in a board string and creates all the nodes
            $node = new Node($board[0]);
            $node->loc = $this->locList[0];
            $this->root = $node;
            for ($i = 1; $i < strlen($board); $i++) {
                    $node->next = new Node($board[$i]);
                    $node->next->loc = $this->locList[$i];
                    $node = $node->next;
            }
            // Load in a dictionary file
            // Use regexp to elimate all the words that could never appear and load the 
            // rest of the words into wordList
            $handle = fopen("dict.txt", "r");
            if ($handle) {
                while (($line = fgets($handle)) !== false) {
                    // process the line read.
                    $line = trim($line);
                    if (strlen($line) > 2) {
                        $this->wordList[] = trim($line);
                    }
                }
                fclose($handle);
            } else {
                // error opening the file.
                echo "Problem with the file.";
            } 
        }

        function isConnected($node1, $node2) {
        // Determines if 2 nodes are connected on the boggle board

            return (($node1->loc == $node2->loc + 1) || ($node1->loc == $node2->loc - 1) ||
               ($node1->loc == $node2->loc - 9) || ($node1->loc == $node2->loc - 10) || ($node1->loc == $node2->loc - 11) ||
               ($node1->loc == $node2->loc + 9) || ($node1->loc == $node2->loc + 10) || ($node1->loc == $node2->loc + 11)) ? true : false;

        }

        function find($value, $notInLoc = []) {
            // Returns a node with the value that isn't in a location
            $current = $this->root;
            while($current) {
                if ($current->value == $value && !in_array($current->loc, $notInLoc)) {
                    return $current;
                }
                if (isset($current->next)) {
                    $current = $current->next;
                } else {
                    break;
                }
            }
            return false;
        }

        function findAll($value) {
            // Returns an array of nodes with a specific value
            $current = $this->root;
            $foundNodes = [];
            while ($current) {
                if ($current->value == $value) {
                    $foundNodes[] = $current;
                }
                if (isset($current->next)) {
                    $current = $current->next;
                } else {
                    break;
                }
            }
            return (empty($foundNodes)) ? false : $foundNodes;
        }

        function findAllConnectedTo($node, $value, $notInLoc = []) {
            // Returns an array of nodes that are connected to a specific node and 
            // contain a specific value and are not in a certain location
            $nodeList = $this->findAll($value);
            $newList = [];
            if ($nodeList) {
                foreach ($nodeList as $node2) {
                    if (!in_array($node2->loc, $notInLoc) && $this->isConnected($node, $node2)) {
                        $newList[] = $node2;
                    }
                }
            }
            return (empty($newList)) ? false : $newList;
        }



        function inner($word, $list, $i = 0, $notInLoc = []) {
            $i++;
            foreach($list as $node) {
                $notInLoc[] = $node->loc;
                if ($list2 = $this->findAllConnectedTo($node, $word[$i], $notInLoc)) {
                    if ($i == (strlen($word) - 1)) {
                        return true;
                    } else {
                        return $this->inner($word, $list2, $i, $notInLoc);
                    }
                }
            }
            return false;
        }

        function findWord($word) {
            if ($list = $this->findAll($word[0])) {
                return $this->inner($word, $list);
            }
            return false;
        }

        function findAllWords() {
            foreach($this->wordList as $word) {
                if ($this->findWord($word)) {
                    $this->foundWords[] = $word;
                }
            }
        }

        function displayBoard() {
            $current = $this->root;
            for ($i=0; $i < 4; $i++) {
                echo $current->value . " " . $current->next->value . " " . $current->next->next->value . " " . $current->next->next->next->value . "<br />";
                if ($i < 3) {
                    $current = $current->next->next->next->next;
                }
            }
        }

    }

    function randomBoardString() {
        return substr(str_shuffle(str_repeat("abcdefghijklmnopqrstuvwxyz", 16)), 0, 16);
    }

    $myBoggle = new Boggle(randomBoardString());
    $myBoggle->displayBoard();
    $x = microtime(true);
    $myBoggle->findAllWords();
    $y = microtime(true);
    echo ($y-$x);
    var_dump($myBoggle->foundWords);

    ?>
Nate
la source
1

Je sais que je suis vraiment en retard à la fête mais j'ai implémenté, comme exercice de codage, un solveur de boggle dans plusieurs langages de programmation (C ++, Java, Go, C #, Python, Ruby, JavaScript, Julia, Lua, PHP, Perl) et J'ai pensé que quelqu'un pourrait être intéressé par ceux-ci, alors je laisse le lien ici: https://github.com/AmokHuginnsson/boggle-solvers

AmokHuginnsson
la source
1

Voici la solution Utilisation de mots prédéfinis dans la boîte à outils NLTK NLTK a un package nltk.corpus en ce que nous avons un package appelé mots et il contient plus de 2 mots anglais en Lakhs que vous pouvez simplement utiliser tous dans votre programme.

Une fois que vous avez créé votre matrice, convertissez-la en un tableau de caractères et exécutez ce code

import nltk
from nltk.corpus import words
from collections import Counter

def possibleWords(input, charSet):
    for word in input:
        dict = Counter(word)
        flag = 1
        for key in dict.keys():
            if key not in charSet:
                flag = 0
        if flag == 1 and len(word)>5: #its depends if you want only length more than 5 use this otherwise remove that one. 
            print(word)


nltk.download('words')
word_list = words.words()
# prints 236736
print(len(word_list))
charSet = ['h', 'e', 'l', 'o', 'n', 'v', 't']
possibleWords(word_list, charSet)

Production:

eleven
eleventh
elevon
entente
entone
ethene
ethenol
evolve
evolvent
hellhole
helvell
hooven
letten
looten
nettle
nonene
nonent
nonlevel
notelet
novelet
novelette
novene
teenet
teethe
teevee
telethon
tellee
tenent
tentlet
theelol
toetoe
tonlet
toothlet
tootle
tottle
vellon
velvet
velveteen
venene
vennel
venthole
voeten
volent
volvelle
volvent
voteen

J'espère que vous l'aurez compris.

lave kumar
la source
0

Voici mon implémentation java: https://github.com/zouzhile/interview/blob/master/src/com/interview/algorithms/tree/BoggleSolver.java

La construction de Trie a pris 0 heures, 0 minutes, 1 secondes, 532 millisecondes
La recherche de mots a pris 0 heures, 0 minutes, 0 secondes, 92 millisecondes

eel eeler eely eer eke eker eld eleut elk ell 
elle epee epihippus ere erept err error erupt eurus eye 
eyer eyey hip hipe hiper hippish hipple hippus his hish 
hiss hist hler hsi ihi iphis isis issue issuer ist 
isurus kee keek keeker keel keeler keep keeper keld kele 
kelek kelep kelk kell kelly kelp kelper kep kepi kept 
ker kerel kern keup keuper key kyl kyle lee leek 
leeky leep leer lek leo leper leptus lepus ler leu 
ley lleu lue lull luller lulu lunn lunt lunule luo 
lupe lupis lupulus lupus lur lure lurer lush lushly lust 
lustrous lut lye nul null nun nupe nurture nurturer nut 
oer ore ort ouphish our oust out outpeep outpeer outpipe 
outpull outpush output outre outrun outrush outspell outspue outspurn outspurt 
outstrut outstunt outsulk outturn outusure oyer pee peek peel peele 
peeler peeoy peep peeper peepeye peer pele peleus pell peller 
pelu pep peplus pepper pepperer pepsis per pern pert pertussis 
peru perule perun peul phi pip pipe piper pipi pipistrel 
pipistrelle pipistrellus pipper pish piss pist plup plus plush ply 
plyer psi pst puerer pul pule puler pulk pull puller 
pulley pullus pulp pulper pulu puly pun punt pup puppis 
pur pure puree purely purer purr purre purree purrel purrer 
puru purupuru pus push puss pustule put putt puture ree 
reek reeker reeky reel reeler reeper rel rely reoutput rep 
repel repeller repipe reply repp reps reree rereel rerun reuel 
roe roer roey roue rouelle roun roup rouper roust rout 
roy rue ruelle ruer rule ruler rull ruller run runt 
rupee rupert rupture ruru rus rush russ rust rustre rut 
shi shih ship shipper shish shlu sip sipe siper sipper 
sis sish sisi siss sissu sist sistrurus speel speer spelk 
spell speller splurt spun spur spurn spurrer spurt sput ssi 
ssu stre stree streek streel streeler streep streke streperous strepsis 
strey stroup stroy stroyer strue strunt strut stu stue stull 
stuller stun stunt stupe stupeous stupp sturnus sturt stuss stut 
sue suer suerre suld sulk sulker sulky sull sully sulu 
sun sunn sunt sunup sup supe super superoutput supper supple 
supplely supply sur sure surely surrey sus susi susu susurr 
susurrous susurrus sutu suture suu tree treey trek trekker trey 
troupe trouper trout troy true truer trull truller truly trun 
trush truss trust tshi tst tsun tsutsutsi tue tule tulle 
tulu tun tunu tup tupek tupi tur turn turnup turr 
turus tush tussis tussur tut tuts tutu tutulus ule ull 
uller ulu ululu unreel unrule unruly unrun unrust untrue untruly 
untruss untrust unturn unurn upper upperer uppish uppishly uppull uppush 
upspurt upsun upsup uptree uptruss upturn ure urn uro uru 
urus urushi ush ust usun usure usurer utu yee yeel 
yeld yelk yell yeller yelp yelper yeo yep yer yere 
yern yoe yor yore you youl youp your yourn yoy 

Remarque: J'ai utilisé le dictionnaire et la matrice de caractères au début de ce fil. Le code a été exécuté sur mon MacBookPro, voici quelques informations sur la machine.

Nom du modèle: MacBook Pro
Identifiant du modèle: MacBookPro8,1
Nom du processeur: Intel Core i5
Vitesse du processeur: 2,3 GHz
Nombre de processeurs: 1
Nombre total de cœurs: 2
Cache L2 (par cœur): 256 Ko
Cache L3: 3 Mo
Mémoire: 4 GB
Boot ROM Version: MBP81.0047.B0E
SMC Version (système): 1.68f96

Robin Zou
la source
0

J'ai résolu cela aussi, avec Java. Mon implémentation est longue de 269 lignes et assez facile à utiliser. Vous devez d'abord créer une nouvelle instance de la classe Boggler, puis appeler la fonction de résolution avec la grille comme paramètre. Il faut environ 100 ms pour charger le dictionnaire de 50 000 mots sur mon ordinateur et il trouve les mots en 10-20 ms environ. Les mots trouvés sont stockés dans un ArrayList, foundWords.

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;

public class Boggler {
    private ArrayList<String> words = new ArrayList<String>();      
    private ArrayList<String> roundWords = new ArrayList<String>(); 
    private ArrayList<Word> foundWords = new ArrayList<Word>();     
    private char[][] letterGrid = new char[4][4];                   
    private String letters;                                         

    public Boggler() throws FileNotFoundException, IOException, URISyntaxException {
        long startTime = System.currentTimeMillis();

        URL path = GUI.class.getResource("words.txt");
        BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(new File(path.toURI()).getAbsolutePath()), "iso-8859-1"));
        String line;
        while((line = br.readLine()) != null) {
            if(line.length() < 3 || line.length() > 10) {
                continue;
            }

            this.words.add(line);
        }
    }

    public ArrayList<Word> getWords() {
        return this.foundWords;
    }

    public void solve(String letters) {
        this.letters = "";
        this.foundWords = new ArrayList<Word>();

        for(int i = 0; i < letters.length(); i++) {
            if(!this.letters.contains(letters.substring(i, i + 1))) {
                this.letters += letters.substring(i, i + 1);
            }
        }

        for(int i = 0; i < 4; i++) {
            for(int j = 0; j < 4; j++) {
                this.letterGrid[i][j] = letters.charAt(i * 4 + j);
            }
        }

        System.out.println(Arrays.deepToString(this.letterGrid));               

        this.roundWords = new ArrayList<String>();      
        String pattern = "[" + this.letters + "]+";     

        for(int i = 0; i < this.words.size(); i++) {

            if(this.words.get(i).matches(pattern)) {
                this.roundWords.add(this.words.get(i));
            }
        }

        for(int i = 0; i < this.roundWords.size(); i++) {
            Word word = checkForWord(this.roundWords.get(i));

            if(word != null) {
                System.out.println(word);
                this.foundWords.add(word);
            }
        }       
    }

    private Word checkForWord(String word) {
        char initial = word.charAt(0);
        ArrayList<LetterCoord> startPoints = new ArrayList<LetterCoord>();

        int x = 0;  
        int y = 0;
        for(char[] row: this.letterGrid) {
            x = 0;

            for(char letter: row) {
                if(initial == letter) {
                    startPoints.add(new LetterCoord(x, y));
                }

                x++;
            }

            y++;
        }

        ArrayList<LetterCoord> letterCoords = null;
        for(int initialTry = 0; initialTry < startPoints.size(); initialTry++) {
            letterCoords = new ArrayList<LetterCoord>();    

            x = startPoints.get(initialTry).getX(); 
            y = startPoints.get(initialTry).getY();

            LetterCoord initialCoord = new LetterCoord(x, y);
            letterCoords.add(initialCoord);

            letterLoop: for(int letterIndex = 1; letterIndex < word.length(); letterIndex++) {
                LetterCoord lastCoord = letterCoords.get(letterCoords.size() - 1);  
                char currentChar = word.charAt(letterIndex);                        

                ArrayList<LetterCoord> letterLocations = getNeighbours(currentChar, lastCoord.getX(), lastCoord.getY());

                if(letterLocations == null) {
                    return null;    
                }       

                for(int foundIndex = 0; foundIndex < letterLocations.size(); foundIndex++) {
                    if(letterIndex != word.length() - 1 && true == false) {
                        char nextChar = word.charAt(letterIndex + 1);
                        int lastX = letterCoords.get(letterCoords.size() - 1).getX();
                        int lastY = letterCoords.get(letterCoords.size() - 1).getY();

                        ArrayList<LetterCoord> possibleIndex = getNeighbours(nextChar, lastX, lastY);
                        if(possibleIndex != null) {
                            if(!letterCoords.contains(letterLocations.get(foundIndex))) {
                                letterCoords.add(letterLocations.get(foundIndex));
                            }
                            continue letterLoop;
                        } else {
                            return null;
                        }
                    } else {
                        if(!letterCoords.contains(letterLocations.get(foundIndex))) {
                            letterCoords.add(letterLocations.get(foundIndex));

                            continue letterLoop;
                        }
                    }
                }
            }

            if(letterCoords != null) {
                if(letterCoords.size() == word.length()) {
                    Word w = new Word(word);
                    w.addList(letterCoords);
                    return w;
                } else {
                    return null;
                }
            }
        }

        if(letterCoords != null) {
            Word foundWord = new Word(word);
            foundWord.addList(letterCoords);

            return foundWord;
        }

        return null;
    }

    public ArrayList<LetterCoord> getNeighbours(char letterToSearch, int x, int y) {
        ArrayList<LetterCoord> neighbours = new ArrayList<LetterCoord>();

        for(int _y = y - 1; _y <= y + 1; _y++) {
            for(int _x = x - 1; _x <= x + 1; _x++) {
                if(_x < 0 || _y < 0 || (_x == x && _y == y) || _y > 3 || _x > 3) {
                    continue;
                }

                if(this.letterGrid[_y][_x] == letterToSearch && !neighbours.contains(new LetterCoord(_x, _y))) {
                    neighbours.add(new LetterCoord(_x, _y));
                }
            }
        }

        if(neighbours.isEmpty()) {
            return null;
        } else {
            return neighbours;
        }
    }
}

class Word {
    private String word;    
    private ArrayList<LetterCoord> letterCoords = new ArrayList<LetterCoord>();

    public Word(String word) {
        this.word = word;
    }

    public boolean addCoords(int x, int y) {
        LetterCoord lc = new LetterCoord(x, y);

        if(!this.letterCoords.contains(lc)) {
            this.letterCoords.add(lc);

            return true;
        }

        return false;
    }

    public void addList(ArrayList<LetterCoord> letterCoords) {
        this.letterCoords = letterCoords;
    } 

    @Override
    public String toString() {
        String outputString = this.word + " ";
        for(int i = 0; i < letterCoords.size(); i++) {
            outputString += "(" + letterCoords.get(i).getX() + ", " + letterCoords.get(i).getY() + ") ";
        }

        return outputString;
    }

    public String getWord() {
        return this.word;
    }

    public ArrayList<LetterCoord> getList() {
        return this.letterCoords;
    }
}

class LetterCoord extends ArrayList {
    private int x;          
    private int y;          

    public LetterCoord(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return this.x;
    }

    public int getY() {
        return this.y;
    }

    @Override
    public boolean equals(Object o) {
        if(!(o instanceof LetterCoord)) {
            return false;
        }

        LetterCoord lc = (LetterCoord) o;

        if(this.x == lc.getX() &&
                this.y == lc.getY()) {
            return true;
        }

        return false;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 29 * hash + this.x;
        hash = 24 * hash + this.y;
        return hash;
    }
}
MikkoP
la source
0

J'ai résolu cela en c. Il faut environ 48 ms pour fonctionner sur ma machine (avec environ 98% du temps passé à charger le dictionnaire à partir du disque et à créer le trie). Le dictionnaire est / usr / share / dict / american-english qui contient 62886 mots.

Code source

matzahboy
la source
0

J'ai résolu cela parfaitement et très rapidement. Je l'ai mis dans une application Android. Regardez la vidéo sur le lien Play Store pour la voir en action.

Word Cheats est une application qui «craque» tout jeu de mots de style matriciel. Cette application a été conçue pour m'aider à tricher avec Word Scrambler. Il peut être utilisé pour les recherches de mots, les ruptures, les mots, le moteur de recherche de mots, le crack de mots, les boggles et plus encore!

Il peut être vu ici https://play.google.com/store/apps/details?id=com.harris.wordcracker

Voir l'application en action dans la vidéo https://www.youtube.com/watch?v=DL2974WmNAI

Josh Harris
la source