Jak znaleźć listę możliwych słów z matrycy liter [Boggle Solver]


376

Ostatnio gram na telefonie iPhone grę Scramble. Niektórzy z was mogą znać tę grę jako Boggle. Zasadniczo, gdy gra się rozpoczyna, otrzymujesz macierz takich liter:

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

Celem gry jest znalezienie jak największej liczby słów, które można utworzyć, łącząc ze sobą litery. Możesz zacząć od dowolnej litery, a wszystkie otaczające ją litery są uczciwą grą, a następnie, po przejściu do następnej litery, wszystkie litery otaczające tę literę są uczciwe, z wyjątkiem wcześniej używanych liter . Więc w siatce powyżej, na przykład, mogę wymyślić słów LOB, TUX, SEA, FAME, itd. Słowa muszą mieć co najmniej 3 znaki i nie więcej niż znaków NxN, co byłoby 16 w tej grze, ale mogą się różnić w niektórych implementacjach . Chociaż ta gra jest zabawna i wciągająca, najwyraźniej nie jestem w tym zbyt dobry i chciałem trochę oszukać, tworząc program, który dałby mi najlepsze możliwe słowa (im dłuższe słowo, tym więcej punktów otrzymasz).

Próbka Boggle
(źródło: boggled.org )

Niestety nie jestem zbyt dobry w algorytmach, ich wydajności i tak dalej. Moja pierwsza próba wykorzystuje słownik taki jak ten (~ 2,3 MB) i wykonuje wyszukiwanie liniowe, próbując dopasować kombinacje do pozycji słownika. Znalezienie możliwych słów zajmuje bardzo dużo czasu, a ponieważ masz tylko 2 minuty na rundę, jest to po prostu nieodpowiednie.

Interesuje mnie, czy którykolwiek Stackoverflowers może wymyślić bardziej wydajne rozwiązania. Głównie szukam rozwiązań wykorzystujących Big 3 Ps: Python, PHP i Perl, chociaż wszystko w Javie lub C ++ też jest fajne, ponieważ szybkość jest niezbędna.

AKTUALNE ROZWIĄZANIA :

  • Adam Rosenfield, Python, ~ 20s
  • John Fouhy, Python, ~ 3s
  • Kent Fredric, Perl, ~ 1s
  • Darius Bacon, Python, ~ 1s
  • rvarcher, VB.NET (link na żywo) , ~ 1s
  • Paolo Bergantino, PHP (link na żywo) , ~ 5s (~ 2s lokalnie)

18
prośba o wyróżnienie MOAR PUZZLES
Kent Fredric

6
Jeśli chodzi o terminy: w moim rozwiązaniu praktycznie cały czas spędzam na budowaniu trie. Po zbudowaniu trie można ją wielokrotnie wykorzystać. Gdyby rozwiązać tylko jedną zagadkę, bardziej efektywne byłoby zastosowanie prostszej struktury danych (takiej jak zestaw wszystkich słów i wszystkich prefiksów).
Adam Rosenfield

3
Ponadto Adam ma większy słownik, o czym świadczy liczba dłuższych słów używanych w jego rozwiązaniu. Wszystkie powinny zostać przetestowane na podstawie wspólnego słownika.
Rich Bradshaw

2
Chyba nikt nie gra dużo Boggle? „Qu” to jedna „litera” i nie jestem pewien, ile rozwiązań uchwyciło tak mało szczegółów. Wygląda na to, że niektóre z nich pozwoliłyby na niezależne korzystanie z „u” między innymi problemami.
Qsario

2
Niedawno miałem to pytanie do wywiadu i ładnie utknąłem w szczegółach. Traktowałem to jako problem z grafem, co jest w porządku, ale tutaj rozwiązania zajmują znacznie mniej miejsca. Teraz tworzę własne rozwiązanie. Gratulacje dla wszystkich, którzy przyczynili się!
Peter Friend,

Odpowiedzi:


143

Moja odpowiedź działa podobnie jak inne tutaj, ale opublikuję ją, ponieważ wygląda na to, że wygląda trochę szybciej niż inne rozwiązania Python, od szybszego konfigurowania słownika. (Sprawdziłem to z rozwiązaniem Johna Fouhy.) Po ustawieniu czas na rozwiązanie jest coraz niższy.

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)

Przykładowe użycie:

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

Edycja: odfiltruj słowa o długości mniejszej niż 3 litery.

Edycja 2: Byłem ciekawy, dlaczego rozwiązanie Perla Kenta Fredrica było szybsze; okazuje się, że używa zestawu dopasowań wyrażeń regularnych zamiast zestawu znaków. Robiąc to samo w Pythonie, podwajając prędkość.


Program daje mi tylko 1 słowo. Dlaczego?
Paolo Bergantino,

Nie chciałem utopić się w produkcji. Zobacz komentarz na dole.
Darius Bacon

6
Lub uzyskaj wszystkie słowa bez ścieżek: print '' .join (posortowane (zestaw (słowo dla (słowo, ścieżka) w rozwiązaniu ())))
Darius Bacon

2
Większość czasu spędza się na analizie słownika. Przetworzyłem to wstępnie do pliku „wordlines.py”, który jest tylko listą, w której każde słowo jest elementem. Ponieważ jest to plik .py, który zostanie przekształcony w plik .pyc. Więc importuję to zamiast read (). Splitlines (). Z tym, na moim pudełku, rozwiązuję go w około dziesiątej części sekundy.
Sean Reifschneider,

1
@shellscape, to kod w języku Python 2. Python 3 porzucił możliwość dekonstruowania argumentów, takich jak def neighbors((x, y))(bezcelowo, o ile widzę). Wymaga także nawiasów wokół argumentu do print.
Darius Bacon

116

Najszybszym rozwiązaniem, jakie otrzymasz, będzie prawdopodobnie przechowywanie słownika w wersji próbnej . Następnie należy utworzyć kolejkę tripletów ( x , y , y ), gdzie każdy element kolejka odpowiada prefiks s słowem, które mogą być podane w siatce, kończąc w miejscu ( x , y ). Zainicjuj kolejkę za pomocą N x N elementów (gdzie N jest rozmiarem twojej siatki), po jednym elemencie na każdy kwadrat w siatce. Następnie algorytm działa w następujący sposób:

Podczas gdy kolejka nie jest pusta:
  Usuń z listy potrójne (x, y, s)
  Dla każdego kwadratu (x ', y') z literą c sąsiadującą z (x, y):
    Jeśli s + c jest słowem, wypisz s + c
    Jeśli s + c jest prefiksem słowa, wstaw (x ', y', s + c) do kolejki

Jeśli przechowujesz słownik w trie, sprawdzanie, czy s + c jest słowem lub przedrostkiem słowa, można wykonać w stałym czasie (pod warunkiem, że zachowasz również dodatkowe metadane w każdej bazie danych kolejki, takie jak wskaźnik do bieżącego węzła w trie), więc czas działania tego algorytmu wynosi O (liczba słów, które można przeliterować).

[Edytuj] Oto implementacja w Pythonie, którą właśnie kodowałem:

#!/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

Przykładowe użycie:

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

Wynik:

[„fa”, „xi”, „ie”, „io”, „el”, „am”, „ax”, „ae”, „aw”, „mi”, „ma”, „me”, „ lo ”,„ li ”,„ oe ”,„ ox ”,„ em ”,„ ea ”,„ ea ”,„ es ”,„ wa ”,„ my ”,„ wa ”,„ bo ”,„ bu ” , „as”, „aw”, „ae”, „st”, „se”, „sa”, „tu”, „ut”, „fam”, „fae”, „imi”, „eli”, „ wiąz, „łok”, „ami”, „ama”, „ame”, „aes”, „szydło”, „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”, „ awl ”,„ awa ”,„ awe ”,„ awa ”,„ aes ”,„ swa ”,„ swa ”,„ sew ”,„ sea ”,„ sea ”,„ saw ”,„ tux ”,„ tub ” , „tut”, „twa”, „twa”, „tst”, „utu”, „fama”, „fame”, „ixil”, „imam”, „amli”, „amil”, „ambo”, „ axil ”,„ axis ”,„ mimi ”,„ mima ”,„ mime ”,„ milo ”,„mile ”,„ mewl ”,„ mese ”,„ mesa ”,„ lolo ”,„ lobo ”,„ lima ”,„ lipa ”,„ kończyna ”,„ lile ”,„ oime ”,„ oleo ”,„ olio ” , „obój”, „obol”, „emim”, „emil”, „wschód”, „łatwość”, „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 ']

Uwagi: Ten program nie wyświetla słów 1-literowych ani nie filtruje według długości słów. Jest to łatwe do dodania, ale nie bardzo istotne dla problemu. Wysyła również kilka słów kilka razy, jeśli można je przeliterować na wiele sposobów. Jeśli dane słowo można przeliterować na wiele różnych sposobów (najgorszy przypadek: każda litera w siatce jest taka sama (np. „A”), a słowo takie jak „aaaaaaaaaa” znajduje się w słowniku), czas wykonywania będzie strasznie wykładniczy . Filtrowanie duplikatów i sortowanie jest trywialne, ponieważ po zakończeniu algorytmu.


14
Ooo Cieszę się, że ktoś podszedł do talerza. Chociaż to działa, nie „pamięta” litery, której już używał, i wymyśla słowa, które wymagałyby dwukrotnego użycia tej samej litery, co jest niedozwolone. Jako idiota, jak mam to naprawić?
Paolo Bergantino

3
To prawda, że ​​nie pamięta, jakie litery zostały odwiedzone, ale nie zostało to określone w specyfikacji =). Aby to naprawić, musisz dodać do bazy danych kolejki listę wszystkich odwiedzonych lokalizacji, a następnie sprawdzić tę listę przed dodaniem następnego znaku.
Adam Rosenfield

Nie, wewnątrz BoggleWords (). Zamiast przechowywać kwadruplet (x, y, s, n), zapisujesz pięcioruplet (x, y, s, n, l), gdzie l jest listą (x, y) odwiedzonych do tej pory. Następnie sprawdzasz każdy (x2, y2) względem l i akceptujesz go tylko wtedy, gdy nie ma go w l. Następnie dodajesz go do nowego l.
Adam Rosenfield

2
Zrobiłem to również, gdy miałem dość grania w Scramble. Myślę, że rozwiązanie rekurencyjne (DFS zamiast BFS) jest bardziej seksowne, ponieważ możesz po prostu zachować zestaw aktywnych komórek (dzięki czemu nie odwiedzasz tej samej komórki dwa razy). Znacznie starsze niż trzymanie wielu list.
Justin Scheiner

2
Czy nie powinno to wpaść w nieskończoną pętlę? Mam na myśli, powiedzmy (x,y), możliwym naśladowcą jest (x+1,y+1). Następnie (x+1,y+1)jest wypychany do kolejki. Jednak (x,y)również będzie zwolennikiem (x+1,y+1), więc czy to nie doprowadzi do niekończącego się odbicia między nimi?
SexyBeast

39

W celu przyspieszenia słownika istnieje jedna ogólna transformacja / proces, który możesz zrobić, aby znacznie wcześniej ograniczyć porównania słowników.

Biorąc pod uwagę, że powyższa siatka zawiera tylko 16 znaków, niektóre z nich są zduplikowane, możesz znacznie zmniejszyć całkowitą liczbę kluczy w słowniku, po prostu odfiltrowując wpisy zawierające nieosiągalne znaki.

Myślałem, że to oczywista optymalizacja, ale widząc, że nikt tego nie zrobił, wspominam o tym.

Zmniejszyło mnie to ze słownika 200 000 kluczy do zaledwie 2000 kluczy podczas samego wprowadzania. To przynajmniej zmniejsza obciążenie pamięci, a to z pewnością odwzoruje gdzieś wzrost prędkości, ponieważ pamięć nie jest nieskończenie szybka.

Implementacja Perla

Moja implementacja jest nieco trudniejsza, ponieważ przywiązałem dużą wagę do znajomości dokładnej ścieżki każdego wyodrębnionego łańcucha, a nie tylko jego ważności.

Mam też kilka adaptacji, które teoretycznie pozwolą na funkcjonowanie siatki z dziurami i siatek z liniami o różnych rozmiarach (zakładając, że poprawnie wprowadzasz dane wejściowe i jakoś się wyrównuje).

Wczesne filtr jest zdecydowanie najbardziej znaczącym wąskie gardło w mojej aplikacji, jak podejrzewał wcześniej, komentując, że bloats linii go od 1.5s do 7.5s.

Po wykonaniu wydaje się, że wszystkie pojedyncze cyfry mają własne poprawne słowa, ale jestem pewien, że wynika to ze sposobu działania pliku słownika.

Jest nieco rozdęty, ale przynajmniej ponownie używam Tree :: Trie z cpan

Niektóre z nich zostały częściowo zainspirowane istniejącymi implementacjami, niektóre z nich już miałem na myśli.

Konstruktywny krytycyzm i sposoby, w jakie można go poprawić, mile widziane (/ zauważa, że ​​nigdy nie szukał CPAN w poszukiwaniu rozwikłanego solwera , ale wypracowanie go było przyjemniejsze)

zaktualizowane o nowe kryteria

#!/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 ) ;
};

Informacje o łuku / wykonaniu do porównania:

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% ==============================

Więcej bełkotów na temat optymalizacji regex

Optymalizacja wyrażeń regularnych, której używam, jest bezużyteczna dla słowników z wieloma rozwiązaniami, a dla wielu rozwiązań będziesz potrzebować pełnego słownika, a nie wstępnie przyciętego.

Jednak to powiedziawszy, dla jednorazowych rozwiązań, jest naprawdę szybkie. (Wyrażenia regularne Perla są w C! :))

Oto kilka różnych dodatków kodu:

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 niefiltrowane przefiltrowane
niefiltrowane 8,16 - -94%
filtrowane 0,464 1658% -

ps: 8,16 * 200 = 27 minut.


2
Wiem, że zawiodłem klub optymalizacji, ale miałem problemy z szybkością, zanim doszedłem do prawdziwej pracy kodu, a skrócenie czasu wprowadzania z 2s do 1,2s wiele dla mnie znaczy.
Kent Fredric

/ ja zauważyłem, że jest to dziwne, teraz pisanie wyrażeń regularnych i pomijanie zajęło mniej czasu niż dodawanie kluczy do skrótu.
Kent Fredric

Fajna implementacja Perla! Teraz go uruchomię.
Paolo Bergantino

Blerg, z trudem instaluję Tree :: Trie na moim serwerze. :(
Paolo Bergantino

3
Jak wygenerowałeś ten ostatni raport (informacje o łuku / wykonaniu)? Wygląda na przydatne.
jmanning2k

33

Możesz podzielić problem na dwie części:

  1. Pewien rodzaj algorytmu wyszukiwania, który wyliczy możliwe ciągi w siatce.
  2. Sposób sprawdzenia, czy łańcuch jest prawidłowym słowem.

Najlepiej byłoby, gdyby (2) zawierał także sposób sprawdzenia, czy łańcuch jest prefiksem prawidłowego słowa - pozwoli to na przycięcie wyszukiwania i zaoszczędzi całą masę czasu.

Trie Adama Rosenfielda to rozwiązanie (2). Jest elegancki i prawdopodobnie preferowany przez specjalistę od algorytmów, ale dzięki nowoczesnym językom i nowoczesnym komputerom możemy być nieco leniwi. Ponadto, jak sugeruje Kent, możemy zmniejszyć rozmiar naszego słownika, odrzucając słowa, których litery nie występują w siatce. Oto kilka 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

Łał; testowanie prefiksu w czasie stałym. Załadowanie połączonego słownika zajmuje kilka sekund, ale tylko kilka :-) (zauważ, że words <= prefixes)

Teraz, w części (1), jestem skłonny myśleć w kategoriach grafów. Zbuduję słownik, który wygląda mniej więcej tak:

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

tj. graph[(x, y)]jest to zestaw współrzędnych, do których można dotrzeć z pozycji (x, y). Dodam również fikcyjny węzeł, Nonektóry połączy się ze wszystkim.

Budowanie jest trochę niezdarne, ponieważ istnieje 8 możliwych pozycji i musisz sprawdzić granice. Oto odpowiednio nieporadny kod python:

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))

Ten kod tworzy także mapowanie słownika (x,y)na odpowiedni znak. To pozwala mi zamienić listę pozycji w słowo:

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

Na koniec przeprowadzamy wyszukiwanie dogłębne. Podstawowa procedura to:

  1. Wyszukiwanie dociera do określonego węzła.
  2. Sprawdź, czy dotychczasowa ścieżka może być częścią słowa. Jeśli nie, nie badaj dalej tej gałęzi.
  3. Sprawdź, czy dotychczasowa ścieżka jest słowem. Jeśli tak, dodaj do listy wyników.
  4. Zbadaj wszystkie dzieci, które dotychczas nie były na ścieżce.

Pyton:

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)

Uruchom kod jako:

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)

i sprawdź, resaby zobaczyć odpowiedzi. Oto lista słów znalezionych dla Twojego przykładu, posortowanych według rozmiaru:

 ['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']

Ładowanie kodu zajmuje (dosłownie) kilka sekund, ale reszta jest natychmiastowa na moim komputerze.


Bardzo dobrze! Również bardzo szybko. Poczekam, czy ktoś podejdzie do talerza, ale jak na razie twoja odpowiedź wygląda dobrze.
Paolo Bergantino

Jestem zdezorientowany, dlaczego „embole” jest twoim jedynym 6-literowym słowem, mam na to 10 różnych słów. Wygląda na to, że nie możesz dwukrotnie odwiedzać tego samego węzła, a jak stwierdził PO, to uczciwa gra.
Kent Fredric

1
ok, prawdopodobnie nadal ma błąd, ponieważ odrzuca „FAMBLE”, „WAMBLE” i „SEMBLE”, które nie udostępniają postaci.
Kent Fredric

Dobrze zauważony! Błąd polegał na tworzeniu zestawu prefiksów: musiałem użyć range(len(w)+1)zamiast range(len(w)). Twierdziłem, żewords <= prefixes ale najwyraźniej nie testowałem tego: - /
John Fouhy

1
Pomogło mi to nauczyć się, jak działa DFS i jak go wdrożyć. Nie byłam pewna, jak wyrazić wdzięczność za to inaczej niż za pomocą komentarza. Dzięki!
Graham Smith

23

Moja próba w Javie. Odczytanie pliku i zbudowanie trie zajmuje około 2 sekund, a rozwiązanie zagadki około 50 ms. Użyłem słownika podanego w pytaniu (zawiera kilka słów, których nie znałem w języku angielskim, takich jak 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

Kod źródłowy składa się z 6 klas. Zamieszczę je poniżej (jeśli nie jest to właściwa praktyka na StackOverflow, proszę powiedz mi).

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;
    }
}

1
Porównywałem moje wyniki z wynikami innych StackOverflowers i wygląda na to, że Adamowi, Johnowi i Rvarcherowi brakuje niektórych słów. Na przykład „Mwa” znajduje się w słowniku (tak!), Ale nie jest zwracane w danych wyjściowych od Adama, Johna i Rvarchera. Jest on zwracany dwukrotnie w łączu PHP Paolo.
gineer

1
Próbowałem tego, wklejając go. Mówi „Czytanie ...” i „Zakończ czytanie ...”, ale potem nic się nie pojawia. Brak pasujących wyników.
MikkoP,

23

Myślę, że prawdopodobnie poświęcisz większość czasu na dopasowanie słów, których nie można zbudować za pomocą siatki listów. Pierwszą rzeczą, którą chciałbym zrobić, jest przyspieszenie tego kroku, co powinno zapewnić ci większość możliwości.

W tym celu ponownie wyraziłbym siatkę jako tabelę możliwych „ruchów”, które indeksujesz według literowanego przejścia, na które patrzysz.

Zacznij od przypisania każdej literze numeru z całego alfabetu (A = 0, B = 1, C = 2, ... itd.).

Weźmy ten przykład:

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

I na razie, użyjmy alfabetu liter, które mamy (zwykle prawdopodobnie za każdym razem będziesz chciał używać tego samego alfabetu):

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

Następnie tworzysz tablicę boolowską 2D, która informuje, czy dostępne jest pewne przejście literowe:

     |  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

Teraz przejrzyj listę słów i przekonwertuj słowa na przejścia:

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

Następnie sprawdź, czy te przejścia są dozwolone, sprawdzając je w tabeli:

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

Jeśli wszystkie są dozwolone, istnieje szansa, że ​​to słowo zostanie odnalezione.

Na przykład słowo „kask” można wykluczyć przy 4. przejściu (m do e: helMEt), ponieważ ten wpis w tabeli jest fałszywy.

I słowo chomik można wykluczyć, ponieważ pierwsze przejście (h do a) jest niedozwolone (nawet nie istnieje w twojej tabeli).

Teraz, dla prawdopodobnie niewielu pozostałych słów, których nie wyeliminowałeś, spróbuj znaleźć je w siatce w sposób, w jaki to robisz teraz lub jak sugerowano w niektórych innych odpowiedziach tutaj. Ma to na celu uniknięcie fałszywych trafień wynikających ze skoków między identycznymi literami w siatce. Na przykład słowo „pomoc” jest dozwolone w tabeli, ale nie w siatce.

Kilka dalszych wskazówek dotyczących poprawy wydajności tego pomysłu:

  1. Zamiast korzystać z tablicy 2D, użyj tablicy 1D i po prostu sam oblicz indeks drugiej litery. Tak więc zamiast tablicy 12x12 jak powyżej, utwórz tablicę 1D o długości 144. Jeśli zawsze będziesz używać tego samego alfabetu (tj. Tablicy 26x26 = 676x1 dla standardowego alfabetu angielskiego), nawet jeśli nie wszystkie litery pojawiają się na twojej siatce , możesz wstępnie obliczyć indeksy do tej tablicy 1D, którą musisz przetestować, aby dopasować słowa ze słownika. Na przykład, wskaźniki dla „cześć” w powyższym przykładzie byłyby

    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. Rozszerz pomysł na tabelę 3D (wyrażoną jako tablica 1D), tzn. Wszystkie dozwolone kombinacje 3-literowe. W ten sposób możesz natychmiast wyeliminować jeszcze więcej słów i zmniejszyć liczbę wyszukiwań tablicowych dla każdego słowa o 1: Aby „cześć”, potrzebujesz tylko 3 wyszukiwań tablic: hel, ell, llo. Nawiasem mówiąc, zbudowanie tego stołu będzie bardzo szybkie, ponieważ w twojej sieci jest tylko 400 możliwych 3-literowych ruchów.

  3. Oblicz wstępnie wskaźniki ruchów w siatce, które musisz uwzględnić w tabeli. W powyższym przykładzie musisz ustawić następujące wpisy na „Prawda”:

    (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. Reprezentuj również swoją siatkę gry w tablicy 1-D z 16 wpisami, a tabela powinna być wstępnie obliczona w 3. zawierać indeksy w tej tablicy.

Jestem pewien, że jeśli zastosujesz to podejście, możesz sprawić, że Twój kod będzie działał niesamowicie szybko, jeśli masz słownik wstępnie obliczony i już załadowany do pamięci.

BTW: Inną przyjemną rzeczą, jeśli budujesz grę, jest uruchamianie tego rodzaju rzeczy natychmiast w tle. Rozpocznij generowanie i rozwiązywanie pierwszej gry, gdy użytkownik nadal patrzy na ekran tytułowy aplikacji i ustawia palec na pozycji, aby nacisnąć „Graj”. Następnie wygeneruj i rozwiąż następną grę, gdy użytkownik gra w poprzednią. To powinno dać ci dużo czasu na uruchomienie kodu.

(Podoba mi się ten problem, więc prawdopodobnie będę miał ochotę wdrożyć moją propozycję w Javie w najbliższych dniach, aby zobaczyć, jak by to faktycznie działało ... opublikuję kod tutaj, kiedy to zrobię).

AKTUALIZACJA:

Ok, miałem dzisiaj trochę czasu i wdrożyłem ten pomysł w Javie:

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();
  }
}

Oto kilka wyników:

W przypadku siatki z obrazka opublikowanego w pierwotnym pytaniu (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

Dla listów opublikowanych jako przykład w pierwotnym pytaniu (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

Dla następującej siatki 5x5:

R P R I T
A H H L N
I E T E P
Z R Y S G
O G W E Y

daje to:

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

W tym celu wykorzystałem listę słów turniejowych TWL06 , ponieważ link w pierwotnym pytaniu już nie działa. Ten plik ma 1,85 MB, więc jest trochę krótszy. A buildDictionaryfunkcja wyrzuca wszystkie słowa zawierające mniej niż 3 litery.

Oto kilka uwag na temat wydajności tego:

  • Jest około 10 razy wolniejszy niż raportowana wydajność implementacji OCaml Victora Nicolleta. Niezależnie od tego, czy jest to spowodowane przez inny algorytm, krótszy słownik, którego używa, fakt, że jego kod jest kompilowany, a mój działa na maszynie wirtualnej Java, lub wydajność naszych komputerów (moim jest Intel Q6600 @ 2,4 MHz z systemem WinXP), Nie wiem Jest to jednak znacznie szybsze niż wyniki dla innych implementacji cytowane na końcu pierwotnego pytania. Tak więc, czy ten algorytm jest lepszy od słownika trie, czy nie, nie wiem w tym momencie.

  • Zastosowana metoda checkWordTriplets()tabelowa daje bardzo dobre przybliżenie do rzeczywistych odpowiedzi. Tylko 1 na 3-5 słów, które przejdzie, nie przejdzie checkWords()testu (patrz liczba kandydatów vs. liczba faktycznych słów powyżej).

  • Coś, czego nie widać powyżej: checkWordTriplets()Funkcja zajmuje około 3,65 ms i dlatego jest w pełni dominująca w procesie wyszukiwania. checkWords()Funkcja zajmuje dość dużo pozostałe 0,05-0,20 ms.

  • Czas wykonania checkWordTriplets()funkcji zależy liniowo od wielkości słownika i jest praktycznie niezależny od wielkości płyty!

  • Czas wykonania checkWords()zależy od wielkości planszy i liczby słów, które nie są wykluczone checkWordTriplets().

  • Powyższe checkWords()wdrożenie jest najgłupszą pierwszą wersją, jaką wymyśliłem. Zasadniczo nie jest w ogóle zoptymalizowany. Ale w porównaniu z checkWordTriplets()tym nie ma znaczenia dla całkowitej wydajności aplikacji, więc nie martwiłem się o to. Ale jeśli rozmiar płyty się powiększy, ta funkcja będzie coraz wolniejsza i ostatecznie zacznie mieć znaczenie. Następnie należałoby go również zoptymalizować.

  • Jedną zaletą tego kodu jest jego elastyczność:

    • Możesz łatwo zmienić rozmiar planszy: Zaktualizuj linię 10 i przekaż tablicę ciągów initializeBoard() .
    • Może obsługiwać większe / różne alfabety i radzić sobie z takimi rzeczami, jak traktowanie „Qu” jako jednej litery bez żadnego nakładu wydajności. Aby to zrobić, należałoby zaktualizować wiersz 9 i kilka miejsc, w których znaki są konwertowane na liczby (obecnie po prostu odejmując 65 od wartości ASCII)

Ok, ale myślę, że do tej pory ten post jest wystarczająco długi. Z całą pewnością mogę odpowiedzieć na wszelkie pytania, ale przejdźmy do komentarzy.


Niezła odpowiedź. Chciałbym zobaczyć twoją implementację w Javie.
MikkoP

@MikkoP Gotowe! :) Zajęło około 3 godzin i 220 linii kodu. Dobry sposób na spędzenie popołudnia. Daj mi znać, jeśli masz jakieś pytania dotyczące tego, jak to działa ... :)
Markus A.,

Dziękujemy za opublikowanie kodu! Wypróbowałem to z własnym słownikiem po dodaniu brakującego importu. W wierszu pojawia się wyjątek ArrayIndexOutOfBoundException ok = possibleTriplets[entry.triplets[t]];. hmmm
MikkoP,

@MikkoP Ten kod jest obecnie napisany, aby zakładać, że słownik zawiera tylko wielkie litery AZ. Sedno jest w linii 34: entry.letters[i] = (byte) letter - 65;Po prostu bierze wartość ASCII i odejmuje 65 („A”). Jeśli masz w słowniku Umlauty lub małe litery, da to wartości większe niż 31, które nie są planowane przez ustawienie wielkości alfabetu w linii 9. Aby obsługiwać inne litery, musisz rozwinąć ten wiersz aby zamapować je w zakresie dozwolonym przez rozmiar alfabetu.
Markus A.,

1
@AlexanderN Prawdopodobnie rozumiesz logikę poprawnie. Popełniłem błąd kopiując siatkę listów ... Przepraszam ... (naprawiono)
Markus A.

19

Zaskakujące, nikt nie próbował tej wersji PHP.

To jest działająca wersja PHP rozwiązania Pythona Johna Fouhy.

Chociaż wziąłem pewne wskazówki z odpowiedzi wszystkich innych, jest to w większości skopiowane od Johna.

$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);

Oto link na żywo jeśli chcesz go wypróbować. Chociaż zajmuje to ~ 2s na moim komputerze lokalnym, zajmuje ~ 5s na moim serwerze internetowym. W obu przypadkach nie jest to bardzo szybkie. Mimo to jest to dość ohydne, więc mogę sobie wyobrazić, że czas można znacznie skrócić. Wszelkie wskazówki dotyczące tego, jak to osiągnąć, będą mile widziane. Brak krotek w PHP sprawił, że współrzędne były dziwne, a moja niemożność zrozumienia, co się do cholery dzieje, wcale nie pomogła.

EDYCJA : Kilka poprawek sprawia, że ​​lokalnie zajmuje to mniej niż 1 sekundę.


+1 @ "i moja niezdolność do zrozumienia, co się do cholery dzieje, wcale nie pomogło." lol. Kocham szczerość!
dna123 16.04.2009

Nie znam PHP, ale pierwszą rzeczą, której spróbuję, jest wyciągnięcie „/ [”. implode („”, $ alfabet). '] {3,} $ /' poza pętlą. To znaczy, ustaw zmienną na to i użyj zmiennej zamiast tego w pętli.
Darius Bacon

Jestem prawie pewien, że PHP przechowuje globalną pamięć podręczną skompilowanych wyrażeń regularnych dla każdego wątku, ale i tak spróbuję.
Paolo Bergantino

1
@Daniel: Najwyraźniej to mój serwer internetowy. To się nie zdarza, gdy uruchamiam lokalnie. Wzruszać ramionami. Naprawdę nie mam ochoty go ścigać.
Paolo Bergantino,

2
Co na końcu należy ustawić jako parametr 7. w funkcji find_words?
MikkoP

16

Nie interesuje Cię VB? :) Nie mogłem się oprzeć. Rozwiązałem to inaczej niż wiele przedstawionych tutaj rozwiązań.

Moje czasy to:

  • Ładowanie słownika i prefiksów słów do tablicy mieszającej: 0,5 do 1 sekundy.
  • Znalezienie słów: uśrednianie poniżej 10 milisekund.

EDYCJA: Czasy ładowania słownika na serwerze hosta działają o około 1 do 1,5 sekundy dłużej niż na moim komputerze domowym.

Nie wiem, jak źle czasy się pogorszą przy obciążeniu serwera.

Moje rozwiązanie napisałem jako stronę internetową w .Net. myvrad.com/boggle

Korzystam ze słownika wymienionego w pierwotnym pytaniu.

Litery nie są ponownie używane w słowie. Znaleziono tylko słowa o długości 3 znaków lub dłuższe.

Używam hashtable wszystkich unikalnych prefiksów i słów zamiast trie. Nie wiedziałem o trie, więc się tam czegoś nauczyłem. Pomysł stworzenia listy prefiksów słów oprócz pełnych słów jest tym, co ostatecznie sprowadziło mój czas na znaczną liczbę.

Przeczytaj komentarze do kodu, aby uzyskać dodatkowe informacje.

Oto kod:

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

Zakładam, że użyłeś systemu ap zamiast [x] [y], ponieważ ten drugi jest dość skomplikowany w VB? Spędziłem dzień próbując uzyskać w ten sposób dynamiczną tablicę 2-kierunkową, tj .: tablica (tablica (1, „cześć”), 1, „cześć”, tablica ()), wciąż nie wiem jak to zrobić że: P
Kent Fredric

W PHP i Perlu 2 przyciemnione tablice są zabawne. Można to zrobić w VB, ale nie nazwałbym tego zabawnym procesem. Dim Arr (,) As Integer = {{1,1}, {0,0}}. Proces AP wyrósł ze mnie, stawiając się na planszy i pytając: „gdzie mogę stąd iść?”. Wiem, że to sztywne rozwiązanie, ale działa tutaj.
rvarcher 16.04.2009

Och, podoba mi się VB.NET ... Próbowałem URL, ale to nie działało. Musiałem samodzielnie skompilować kod jako Windows Forms i to działa. Dzięki.
Ahmed Eissa,

11

Gdy tylko zobaczyłem opis problemu, pomyślałem „Trie”. Ale widząc, jak kilka innych plakatów wykorzystało to podejście, szukałem innego podejścia, aby być innym. Niestety, podejście Trie działa lepiej. Uruchomiłem Perla Kenta na moim komputerze i uruchomienie go zajęło 0,31 sekundy, po dostosowaniu go do użycia pliku słownika. Uruchomienie mojej własnej implementacji perla wymagało 0,54 sekundy.

To było moje podejście:

  1. Utwórz skrót przejścia, aby modelować legalne przejścia.

  2. Iteruj przez wszystkie 16 ^ 3 możliwe trzyliterowe kombinacje.

    • W pętli wykluczaj nielegalne przejścia i powtarzaj wizyty na tym samym placu. Utwórz wszystkie trzyliterowe sekwencje prawne i przechowuj je w haszu.
  3. Następnie przejdź przez wszystkie słowa w słowniku.

    • Wyklucz słowa, które są za długie lub za krótkie
    • Przesuń 3-literowe okno po każdym słowie i sprawdź, czy należy ono do 3-literowych kombinacji z kroku 2. Wyklucz słowa, które się nie powiodły. To eliminuje większość niedopasowań.
    • Jeśli nadal nie zostanie wyeliminowany, użyj algorytmu rekurencyjnego, aby sprawdzić, czy słowo można utworzyć, tworząc ścieżki przez łamigłówkę. (Ta część jest powolna, ale rzadko wywoływana).
  4. Wydrukuj znalezione słowa.

    Próbowałem sekwencji 3-literowych i 4-literowych, ale sekwencje 4-literowe spowolniły program.

W moim kodzie używam / usr / share / dict / words do mojego słownika. Jest standardem w systemie Mac OS X i wielu systemach uniksowych. Możesz użyć innego pliku, jeśli chcesz. Aby złamać inną łamigłówkę, po prostu zmień zmienną @puzzle. Łatwo byłoby to dostosować do większych matryc. Musisz tylko zmienić skrót mieszania% przejść i skrót mieszania% legalTransitions.

Zaletą tego rozwiązania jest to, że kod jest krótki, a struktury danych proste.

Oto kod Perla (wiem, że używa zbyt wielu zmiennych globalnych):

#!/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.
}

Czy zmieniła się lokalizacja słownika? Próbowałem znaleźć słowa ze słownika, ponieważ chciałem porównać moje rozwiązanie ze wszystkimi, ale nie znalazłem go pod podanym linkiem w / usr / share / dict. Wiem, że to dość stary wątek, ale wspaniale, jeśli możesz mnie wskazać. Z góry dziękuje za twoją pomoc.
Naman

Nie mam w tej chwili mojego Maca pod ręką. Wszystko czego potrzebujesz to plik z angielskimi słowami, jeden do wiersza, oddzielone znakiem nowej linii. Możesz znaleźć taki plik w Internecie. Jeden jest tutaj: mieliestronk.com/corncob_lowercase.txt, ale prawdopodobnie istnieją listy zawierające więcej słów.
Paul Chernoch

Wielkie dzięki za odpowiedź. Znalazłem to w plikach ubuntu.
Naman

9

Wiem, że się spóźniłem, ale jeden z nich zrobiłem jakiś czas temu w PHP - dla zabawy też ...

http://www.lostsockdesign.com.au/sandbox/boggle/index.php?letters=fxieamloewbxastu Znaleziono 75 słów (133 pkt) w 0,90108 sekund

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

Daje pewne wskazówki na temat tego, co faktycznie robi program - każda litera rozpoczyna przeglądanie wzorów, a każda „.” pokazuje ścieżkę, którą próbował obrać. Więcej '.' istnieje dalsze wyszukiwanie.

Daj mi znać, jeśli chcesz kod ... to straszna mieszanka PHP i HTML, która nigdy nie miała ujrzeć światła dziennego, więc nie ważę się go tutaj nie publikować: P


9

Spędziłem 3 miesiące pracując nad rozwiązaniem problemu 10 najlepszych gęstych punktów 5x5 tablic Boggle.

Problem został już rozwiązany i przedstawiony z pełnym ujawnieniem na 5 stronach internetowych. Proszę o kontakt z pytaniami.

Algorytm analizy tablic używa jawnego stosu, aby pseudo-rekurencyjnie przechodzić przez kwadraty planszy przez Directed Acyclic Word Graph z bezpośrednimi informacjami potomnymi i mechanizmem śledzenia znaczników czasu. Może to być najbardziej zaawansowana struktura danych leksykalnych na świecie.

Schemat ocenia około 10 000 bardzo dobrych płyt na sekundę na poczwórnym rdzeniu. (9500 punktów)

Nadrzędna strona internetowa:

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

Składowe strony internetowe:

Optymalna tablica wyników - http://www.pathcom.com/~vadco/binary.html

Zaawansowana struktura leksykonu - http://www.pathcom.com/~vadco/adtdawg.html

Algorytm analizy płytki - http://www.pathcom.com/~vadco/guns.html

Równoległe przetwarzanie wsadowe - http://www.pathcom.com/~vadco/parallel.html

- Ten kompleksowy zakres prac zainteresuje tylko osobę, która wymaga tego, co najlepsze.


4
Twoja analiza jest interesująca, ale twoje wyniki nie są technicznie tablicami Boggle. Gra boggle 5x5 zawiera jedną kość, która zawiera twarze BJKQXZ, twoja implementacja wyraźnie wyklucza wszystkie te litery, więc pozycja planszy nie jest w rzeczywistości możliwa w prawdziwej grze Boggle.
MarkPflug

4

Czy Twój algorytm wyszukiwania stale zmniejsza listę słów w miarę wyszukiwania?

Na przykład w powyższym wyszukiwaniu jest tylko 13 liter, od których słowa mogą zaczynać (skutecznie zmniejszając do połowy liczby liter początkowych).

W miarę dodawania kolejnych permutacji liter dalsze zmniejszanie dostępnych zestawów słów zmniejsza konieczność wyszukiwania.

Zacznę tam


4

Musiałbym więcej zastanowić się nad kompletnym rozwiązaniem, ale jako przydatna optymalizacja zastanawiam się, czy warto byłoby wstępnie obliczyć tabelę częstotliwości digramów i trygramów (kombinacje 2- i 3-literowe) na podstawie wszystkich słowa ze słownika i użyj go do ustalenia priorytetu wyszukiwania. Wybrałbym początkowe litery słów. Jeśli więc słownik zawiera słowa „Indie”, „Woda”, „Ekstremalne” i „Niezwykłe”, wówczas obliczona tabela może być następująca:

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

Następnie wyszukaj te digramy w kolejności podobieństwa (najpierw EX, potem WA / IN)


4

Najpierw przeczytaj, jak jeden z projektantów języka C # rozwiązał powiązany problem: http://blogs.msdn.com/ericlippert/archive/2009/02/04/a-nasality-talisman-for-the-sultana-analyst.aspx .

Podobnie jak on, możesz zacząć od słownika i kanonizować słowa, tworząc słownik od tablicy liter posortowanych alfabetycznie do listy słów, które można przeliterować na podstawie tych liter.

Następnie zacznij tworzyć możliwe słowa z tablicy i przeglądaj je. Podejrzewam, że doprowadzi cię to dość daleko, ale z pewnością jest więcej sztuczek, które mogą przyspieszyć.


4

Sugeruję utworzenie drzewa liter na podstawie słów. Drzewo będzie się składać z struktur literowych, takich jak to:

letter: char
isWord: boolean

Następnie budujesz drzewo, a każda głębokość dodaje nową literę. Innymi słowy, na pierwszym poziomie byłby alfabet; następnie z każdego z tych drzew będzie kolejnych 26 wpisów i tak dalej, dopóki nie przeliterujesz wszystkich słów. Trzymaj się tego przeanalizowanego drzewa, a wszystkie możliwe odpowiedzi będą szybciej wyszukiwane.

Za pomocą tego przeanalizowanego drzewa możesz bardzo szybko znaleźć rozwiązania. Oto pseudo-kod:

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.

Można to przyspieszyć odrobiną dynamicznego programowania. Na przykład w twojej próbce dwa „A” znajdują się obok „E” i „W”, które (od punktu, w który je uderzyły) byłyby identyczne. Nie mam wystarczająco dużo czasu, aby naprawdę przeliterować kod, ale myślę, że możesz to zrozumieć.

Ponadto jestem pewien, że znajdziesz inne rozwiązania, jeśli Google dla „Boggle solver”.



3

Wesoły. Kilka dni temu prawie napisałem to samo pytanie z powodu tej samej cholernej gry! Nie zrobiłem tego jednak, ponieważ właśnie szukałem w Google Pythona w języku boggle solver i uzyskałem wszystkie odpowiedzi, których mogłem chcieć.


Nie wiedziałem, że popularna nazwa to „boggle”, ale znalazłem trochę rzeczy w google, byłem ciekawy, co ludzie wymyślą na SO. :)
Paolo Bergantino

3

Zdaję sobie sprawę, że czas na to pytanie minął, ale ponieważ sam pracowałem nad solwerem i natknąłem się na to podczas przeglądania Google, pomyślałem, że powinienem opublikować odniesienie do mojego, ponieważ wydaje się ono nieco inne niż niektóre inne.

Zdecydowałem się użyć płaskiej tablicy na planszy i wykonywać rekurencyjne polowania z każdej litery na planszy, przechodząc od ważnego sąsiada do ważnego sąsiada, rozszerzając polowanie, jeśli bieżąca lista liter jest poprawnym prefiksem w indeksie. Podczas przechodzenia przez pojęcie bieżącego słowa jest lista indeksów na tablicy, a nie litery, które składają się na słowo. Podczas sprawdzania indeksu indeksy są tłumaczone na litery i sprawdzanie jest wykonywane.

Indeks jest słownikiem typu brute force, który jest trochę jak trie, ale pozwala na zapytania Pythona o indeks. Jeśli na liście znajdują się słowa „kot” i „cater”, otrzymasz to w słowniku:

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

Więc jeśli bieżącym słowem jest „ca”, wiesz, że jest to poprawny przedrostek, ponieważ 'ca' in dzwraca True (więc kontynuuj przeglądanie tablicy). A jeśli bieżącym słowem jest „kot”, to wiesz, że jest to poprawne słowo, ponieważ jest to poprawny przedrostek i'cat' in d['cat'] zwraca również wartość True.

Jeśli wydaje się, że jest to dozwolone dla pewnego czytelnego kodu, który nie wydaje się zbyt wolny. Podobnie jak wszyscy inni, wydatkiem w tym systemie jest czytanie / budowanie indeksu. Rozwiązanie planszy to w zasadzie hałas.

Kod znajduje się na stronie http://gist.github.com/268079 . Jest celowo pionowy i naiwny, z dużą ilością jawnej kontroli poprawności, ponieważ chciałem zrozumieć problem bez kruszenia go za pomocą magii lub niejasności.


3

Mój solver napisałem w C ++. Zaimplementowałem niestandardową strukturę drzewa. Nie jestem pewien, czy można to uznać za trie, ale jest podobnie. Każdy węzeł ma 26 gałęzi, po 1 na każdą literę alfabetu. Przemierzam gałęzie tablicy boggle równolegle do gałęzi mojego słownika. Jeśli gałąź nie istnieje w słowniku, przestaję ją wyszukiwać na tablicy Boggle. Konwertuję wszystkie litery na tablicy na ints. Zatem „A” = 0. Ponieważ są to tylko tablice, wyszukiwanie zawsze ma wartość O (1). Każdy węzeł przechowuje, jeśli uzupełnia słowo i ile słów istnieje w jego elementach potomnych. Drzewo jest przycinane, ponieważ znaleziono słowa, które ograniczają wielokrotne wyszukiwanie tych samych słów. Uważam, że przycinanie jest również O (1).

Procesor: Pentium SU2700
1,3 GHz RAM: 3 GB

Ładuje słownik 178 590 słów w <1 sekundę.
Rozwiązuje Boggle 100x100 (boggle.txt) w 4 sekundy. Znaleziono około 44 000 słów.
Rozwiązanie Boggle 4x4 jest zbyt szybkie, aby zapewnić znaczący punkt odniesienia. :)

Szybkie Boggle Solver GitHub Repo


2

Biorąc pod uwagę tablicę Boggle z N rzędami i M kolumnami, załóżmy, że:

  • N * M jest znacznie większy niż liczba możliwych słów
  • N * M jest znacznie większy niż najdłuższe możliwe słowo

Przy tych założeniach złożoność tego rozwiązania wynosi O (N * M).

Myślę, że porównywanie czasów działania dla tej jednej przykładowej płyty na wiele sposobów nie ma sensu, ale ze względu na kompletność to rozwiązanie kończy się w <0,2 s na moim nowoczesnym MacBooku Pro.

To rozwiązanie znajdzie wszystkie możliwe ścieżki dla każdego słowa w korpusie.

#!/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}"

2

To rozwiązanie daje również kierunek wyszukiwania w danej tablicy

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

Wynik:

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  → → ↑ ↘ →

Kod:

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)

1

Mam wdrożone rozwiązanie w SML . Wstępnie kompiluje słownik jako trie i wykorzystuje dwuliterowe częstotliwości sekwencji, aby wyeliminować krawędzie, które nigdy nie mogłyby pojawić się w słowie, aby przyspieszyć przetwarzanie.

Rozwiązuje przykładową płytkę w 0,35 ms (z dodatkowym czasem uruchamiania 6 ms, który jest głównie związany z ładowaniem trie do pamięci).

Znalezione rozwiązania:

["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"]

To miłe, ale wszystkie zamieszczone tutaj czasy wymagają czasu „rozruchu”, aby załadować słownik do pamięci, więc porównanie 0,35 z innymi czasami jest dalekie od dokładności. Czy używasz innego słownika? Brakuje Ci słów. Tak czy inaczej, +1
Paolo Bergantino

Czas rozruchu wynosi 6 ms, więc na pełną pracę patrzysz 6,35 ms. Korzystam z lokalnego /usr/share/dictsłownika i niektórych słów rzeczywiście brakuje (np. EMBOLE).
Victor Nicollet

1

Rozwiązanie JavaScript Node.JS. Oblicza wszystkie 100 niepowtarzalnych słów w mniej niż sekundę, w tym czytanie pliku słownika (MBA 2012).

Wynik:
[„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 ”,„ ALE ” , „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”, „kończyna”, „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 ”]EAST ”,„ WAME ”,„ TWAS ”,„ TWAE ”,„ EMIL ”,„ WEAM ”,„ OIME ”,„ AXIL ”,„ WEST ”,„ TWAE ”,„ kończyna ”,„ 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”]EAST ”,„ WAME ”,„ TWAS ”,„ TWAE ”,„ EMIL ”,„ WEAM ”,„ OIME ”,„ AXIL ”,„ WEST ”,„ TWAE ”,„ kończyna ”,„ 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”, „kończyna”, „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”, „kończyna”, „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”, „KOŃCZYNA”, „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”, „KOŃCZYNA”, „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 ”]

Kod:

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
})))

1

Chciałem więc dodać inny sposób rozwiązania tego problemu, ponieważ wszyscy kochają PHP. Chciałbym zrobić trochę refaktoryzacji, na przykład użyć dopasowania wyrażenia przeciwnego do pliku słownika, ale w tej chwili ładuję cały plik słownika do listy słów.

Zrobiłem to, korzystając z pomysłu na listę połączoną. Każdy węzeł ma wartość znaku, wartość lokalizacji i następny wskaźnik.

Wartość lokalizacji jest sposobem, w jaki dowiedziałem się, czy dwa węzły są połączone.

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

Korzystając z tej siatki, wiem, że dwa węzły są połączone, jeśli położenie pierwszego węzła jest równe położeniu drugiego węzła +/- 1 dla tego samego rzędu, +/- 9, 10, 11 dla rzędu powyżej i poniżej.

Używam rekurencji do wyszukiwania głównego. Usuwa słowo z listy słów, wyszukuje wszystkie możliwe punkty początkowe, a następnie rekurencyjnie znajduje następne możliwe połączenie, pamiętając, że nie może przejść do lokalizacji, z której już korzysta (dlatego dodam $ notInLoc).

W każdym razie wiem, że wymaga to trochę refaktoryzacji i chciałbym usłyszeć opinie na temat tego, jak uczynić go czystszym, ale daje prawidłowe wyniki na podstawie pliku słownika, którego używam. W zależności od liczby samogłosek i kombinacji na planszy zajmuje to od 3 do 6 sekund. Wiem, że kiedy już dopasuję wyniki słownika, to znacznie się zmniejszy.

<?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);

    ?>

1

Wiem, że jestem naprawdę spóźniony na imprezie, ale zaimplementowałem, jako ćwiczenie programistyczne, rozwikłany solver w kilku językach programowania (C ++, Java, Go, C #, Python, Ruby, JavaScript, Julia, Lua, PHP, Perl) i Myślałem, że ktoś może być nimi zainteresowany, więc zostawiam link tutaj: https://github.com/AmokHuginnsson/boggle-solvers


1

Oto rozwiązanie Używanie predefiniowanych słów w zestawie narzędzi NLTK NLTK ma pakiet nltk.corpus w tym, że mamy pakiet o nazwie słowa i zawiera on więcej niż dwa słowa angielskie, których możesz po prostu użyć w swoim programie.

Po utworzeniu matrycy przekonwertuj ją na tablicę znaków i wykonaj ten kod

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)

Wynik:

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

Mam nadzieję, że rozumiesz.


0

Oto moja implementacja Java: https://github.com/zouzhile/interview/blob/master/src/com/interview/algorithms/tree/BoggleSolver.java

Kompilacja Trie zajęła 0 godzin, 0 minut, 1 sekundę, 532 milisekund
Wyszukiwanie słowa zajęło 0 godzin, 0 minut, 0 sekund, 92 milisekund

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 

Uwaga: użyłem słownika i matrycy znaków na początku tego wątku. Kod został uruchomiony na moim MacBookPro, poniżej znajduje się kilka informacji o maszynie.

Nazwa modelu: MacBook Pro
Identyfikator modelu: MacBookPro8,1
Nazwa procesora: Intel Core i5
Szybkość procesora: 2,3 GHz
Liczba procesorów: 1
Całkowita liczba rdzeni: 2
L2 Cache (na rdzeń): 256 KB
L3 Cache: 3 MB
Pamięć: 4 GB
Boot ROM Wersja: MBP81.0047.B0E
Wersja SMC (system): 1.68f96


0

Też rozwiązałem to za pomocą Java. Moja implementacja ma 269 linii i jest łatwa w użyciu. Najpierw musisz utworzyć nową instancję klasy Boggler, a następnie wywołać funkcję rozwiązywania z siatką jako parametrem. Załadowanie słownika zawierającego 50 000 słów na moim komputerze zajmuje około 100 ms, a znalezienie słów następuje po około 10-20 ms. Znalezione słowa są przechowywane w 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;
    }
}

0

Rozwiązałem to w ok. Uruchomienie na moim komputerze zajmuje około 48 ms (około 98% czasu spędzonego na ładowaniu słownika z dysku i tworzeniu trie). Słownik to / usr / share / dict / american-english, który ma 62886 słów.

Kod źródłowy


0

Rozwiązałem to doskonale i bardzo szybko. Umieściłem go w aplikacji na Androida. Wyświetl wideo w linku do sklepu Play, aby zobaczyć go w akcji.

Word Cheats to aplikacja, która „łamie” każdą grę słowną w stylu matrycy. Ta aplikacja została stworzona, aby pomóc mi oszukiwać w programie szyfrującym. Może być używany do wyszukiwania słów, ruzzle, słów, wyszukiwarki słów, crackowania słów, boggle i wiele więcej!

Można to zobaczyć tutaj https://play.google.com/store/apps/details?id=com.harris.wordcracker

Zobacz aplikację w akcji na wideo https://www.youtube.com/watch?v=DL2974WmNAI

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.