web-dev-qa-db-ja.com

文字マトリックスから可能な単語のリストを見つける方法[Boggle Solver]

最近私は私のiPhone上でスクランブルと呼ばれるゲームをしています。あなたの何人かはこのゲームをBoggleとして知っているかもしれません。基本的に、ゲームが開始されると、あなたは以下のような文字のマトリックスを取得します。

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

このゲームの目的は、文字を連鎖させることによって形成できる、できるだけ多くの単語を見つけることです。あなたは任意の文字で始めることができます、そしてそれを囲むすべての文字は公正なゲームです、そして次にあなたが次の文字に移動すると、その文字を囲むすべての文字は公正なゲームです以前に使用された文字用。たとえば、上記のグリッドでは、LOBTuxSEAFAMEなどの単語を思いつくことができます。単語は3文字以上、N×N文字以下でなければなりません。実装によっては異なります。このゲームは楽しさと中毒性がありますが、私は明らかにそれがあまり得意ではないと私は私に可能な限り最高の言葉を与えるだろうプログラムを作ることによって少し詐欺したいと思いました。

Sample Boggle
(情報源: boggled.org

私は、残念ながら、アルゴリズムやその効率などにはあまり適していません。私の最初の試みはこのような のような辞書を使います (〜2.3MB)そして辞書エントリとの組み合わせをマッチさせようとしている線形検索をします。これは可能な単語を見つけるのに非常に長い時間がかかり、あなたが1ラウンドあたり2分しか得ないので、それは単に適切ではありません。

私はStackoverflowersがもっと効率的な解決策を思い付くことができるかどうか見ることに興味があります。私は主にBig 3 Pを使用したソリューションを探しています。Python、PHP、およびPerlです。ただし、スピードが不可欠であるため、JavaまたはC++のものもかっこいいです。

現在のソリューション

  • Adam Rosenfield、パイソン、〜20代
  • John Fouhy、パイソン、〜3秒
  • Kent Fredric、Perl、〜1秒
  • ダライアスベーコン、パイソン、〜1秒
  • rvarcher、VB.NET (ライブリンク) 、〜1秒
  • Paolo Bergantino、PHP (ライブリンク) 、〜5秒(ローカルで〜2秒)
373

私の答えは他のものと同じように動作しますが、辞書を早く設定することで、他のPythonのソリューションよりも少し速く見えるのでそれを投稿します。 (私はこれをJohn Fouhyの解決策と照らし合わせてチェックしました。)セットアップの後、解決する時間は騒音の中で下がりました。

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)

使用例

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

編集: 3文字未満の単語を除外します。

編集2 :私は、Kent FredricのPerlソリューションが高速である理由を知りました。一連の文字の代わりに正規表現マッチングを使用することが判明しました。 Pythonでも同じことをするとスピードが2倍になります。

141
Darius Bacon

あなたが手に入れようとしている最も速い解決策はおそらくあなたの辞書を trie に保存することを含むでしょう。次に、トリプレットのキュー(xys)を作成します。キュー内の各要素はプレフィックスに対応しますsグリッドでつづることができるWordの位置(xy)。キューをN x N要素(Nはグリッドのサイズ)で初期化します。各要素に1要素グリッド内の正方形。その後、アルゴリズムは次のように進行します。

キューが空ではない場合:
トリプル(x、y、s)をデキューします。 y):
 s + cが単語の場合は、s + cを出力します。
 s + cが単語の接頭辞の場合は、(x '、y'、s + c)をキュー

辞書にトライを保存している場合、s + cがWordかWordの接頭辞かどうかをテストすることは、一定の時間内に行うことができます(余分なものもある場合)。そのため、このアルゴリズムの実行時間はO(スペル処理可能な単語数)になります。

[編集]これは私がコーディングしたばかりのPythonでの実装です。

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

使用例

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

出力:

['fa'、 'xi'、 'ie'、 'io'、 'el'、 'am'、 'ax'、 'ae'、 'aw'、 'mi'、 'ma'、 'me'、 ' lo '、' li '、' oe '、' ox '、' em '、' ea '、' ea '、' es '、' wa '、' we '、' wa '、' bo '、' bu ' 、「as」、「aw」、「ae」、「st」、「se」、「sa」、「tu」、「ut」、「fam」、「fae」、「imi」、「eli」、「 Elm '、' elb '、' AMI '、' Ame '、' aes '、' awl '、' awa '、' awe '、' awa '、' mix '、' mim '、' mil ' 、 'mam'、 'max'、 'mae'、 'mew'、 'mem'、 'mes'、 'lob'、 'lox'、 'lei'、 'leo'、 'lie'、 ' lim '、' oil '、' olm '、' ewe '、' eme '、' wax '、' waf '、' wae '、' waw '、' wem '、' wea '、' wea '、はでした、 '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'、 'axle'、 'mimi'、 'ミマ、ミメ、ミロ、マイル、メウル、メサ、ロロ、ロボ、リマ、ライム、リム、リル、 'oime'、 'oleo'、 'olio'、 'oboe'、 'emim'、 'emil'、 'east'、 'ease'、 'wame'、 'wawa'、 'wawa'、 ' weam、west、wese、wast、wase 、「wawa」、「wawa」、「boil」、「bole」、「bobo」、「blob」、「bleo」、「bubo」、「asem」、「stub」、「stut」、「 'swam'、 'semi'、 'seme'、 'seam'、 '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」 、「のこぎり」、「のこぎり」

注:このプログラムは1文字の単語を出力したり、単語の長さでフィルタをかけたりすることはまったくありません。これは簡単に追加できますが、実際には問題とは関係ありません。複数の方法で綴ることができる場合も、いくつかの単語を複数回出力します。与えられたWordのスペルをさまざまな方法で設定できる場合(最悪の場合:グリッド内のすべての文字が同じ(例: 'A')、辞書の中に 'aaaaaaaaaa'のようなWordがあります)。 。アルゴリズムが終了した後、重複したものをフィルタリングしてソートすることは簡単です。

116
Adam Rosenfield

辞書を高速化するために、辞書比較を事前に大幅に削減するために実行できる一般的な変換/プロセスが1つあります。

上記のグリッドには16文字しか含まれておらず、その一部が重複していることを考えると、使用できない文字を含むエントリを単純に除外することで、辞書内のキーの総数を大幅に減らすことができます。

私はこれが明らかな最適化だと思いましたが、誰もそれをしていないのを見て私はそれに言及しています。

それは、入力パスの間だけで、私を200,000キーの辞書からたったの2,000キーに減らしました。これは少なくともメモリのオーバーヘッドを減らし、メモリが無限に速くはないので、どこかの速度の増加に確実に対応します。

Perlの実装

私の実装は少し重いです。なぜなら、私はそこでの妥当性だけではなく、抽出されたすべての文字列の正確なパスを知ることができることを重視したからです。

理論的には穴が開いているグリッドと、さまざまなサイズの行があるグリッドを機能させるための適応もいくつかあります(入力が正しく行われ、何らかの形で並んでいると仮定して)。

先に疑われたように、アーリーフィルターは私のアプリケーションでははるかに重要なボトルネックであり、その行は1.5秒から7.5秒に膨れ上がっているとコメントしています。

実行時には、すべての1桁の数字はそれぞれ有効な単語であると考えられますが、辞書ファイルがどのように機能するかに起因していると確信しています。

少々肥大化していますが、少なくとも私は再利用しています Tree :: Trie cpanから

そのうちのいくつかは部分的に既存の実装からインスピレーションを受けました、そのうちのいくつかは私がすでに頭に入れていたものでした。

建設的な批評とそれが改善されることができる方法(/私は彼がしたことがないことに気付く boggle solverのためにCPANを検索しました 、しかしこれは解決するのがより面白かった)

新しい基準に合わせて更新

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

比較のためのアーチ/実行情報:

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

その正規表現の最適化に関する詳細な説明

私が使用している正規表現の最適化は、マルチ解決辞書には役に立ちません。マルチ解決には、事前にトリミングされた辞書ではなく、完全な辞書が必要です。

しかし、それは一回限りの解決策では非常に速いということです。 (Perlの正規表現はCにあります!:))

これは、さまざまなコード追加です。

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');
      }
  });
}
フィルタなしフィルタ処理
フィルタなし8.16  -  -94%
フィルタあり0.464 1658% -  

ps:8.16 * 200 = 27分。

39
Kent Fredric

問題を2つに分割することができます。

  1. グリッド内の可能な文字列を列挙する、ある種の検索アルゴリズム。
  2. 文字列が有効なWordかどうかをテストする方法。

理想的には、(2)は文字列が有効なWordの接頭辞であるかどうかをテストする方法も含むべきです - これはあなたの検索を整理して、時間の全体の節約を可能にするでしょう。

Adam RosenfieldのTrieは(2)の解決策です。それはエレガントで、おそらくあなたのアルゴリズム専門家が望むものですが、現代の言語と現代のコンピュータでは、少し時間がかかることがあります。また、Kentが示唆しているように、グリッド内に存在しない文字を含む単語を捨てることで辞書のサイズを減らすことができます。これが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

ワオ;定時プレフィックステストリンクした辞書をロードするのに数秒かかりますが、ほんの数:-)(words <= prefixesに注意してください)

さて、パート(1)では、グラフの観点から考える傾向があります。だから私はこのようなものに見える辞書を作ります:

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

すなわちgraph[(x, y)]は、位置(x, y)から到達できる座標のセットです。また、すべてに接続するダミーノードNoneを追加します。

8つの可能なポジションがあり、境界チェックをしなければならないので、それを構築することは少し不器用です。これはそれに対応して不器用な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))

このコードはまた、(x,y)を対応する文字にマッピングする辞書を作成します。これにより、ポジションのリストをWordに変換できます。

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

最後に、深さ優先探索を行います。基本的な手順は以下のとおりです。

  1. 検索は特定のノードに到着します。
  2. これまでのパスがWordの一部である可能性があるかどうかを確認してください。そうでない場合は、これ以上このブランチを探索しないでください。
  3. これまでのパスがWordかどうかを確認してください。もしそうなら、結果のリストに追加してください。
  4. これまでの道の一部ではないすべての子供たちを探検してください。

Python:

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

    if Word not in prefixes:
        return

    if Word in words:
        results.add(Word)

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

コードを次のように実行します。

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)

resを調べて答えを確認してください。あなたの例で見つけた単語のリストをサイズ順に並べています:

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

コードは(文字通り)辞書をロードするのに数秒かかりますが、残りは私のマシンでは瞬時です。

33
John Fouhy

おそらくあなたのレターグリッドでは構築できない単語をマッチさせるのにあなたはおそらくあなたの時間の大部分を費やすだろうと思います。だから、私が最初にやることはそのステップをスピードアップしようとすることであり、それはそこにあなたがほとんどの方法を取得する必要があります。

このために、私はあなたが見ている文字遷移によってあなたが索引付けする可能な「移動」の表としてグリッドを再表現するでしょう。

各文字にアルファベット全体から番号を割り当てることから始めます(A = 0、B = 1、C = 2、...など)。

この例を見てみましょう:

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

そして今のところ、私たちが持っている文字のアルファベットを使ってみましょう(通常は毎回同じアルファベットを使いたくなるでしょう)。

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

次に、利用可能な特定の文字遷移があるかどうかを示す2次元ブール配列を作成します。

     |  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

今すぐあなたの単語リストを調べて、単語をトランジションに変換します。

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

それからあなたのテーブルでそれらを調べて、これらの遷移が許されるかどうかチェックしてください:

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

それらがすべて許可されている場合、このWordが見つかる可能性があります。

たとえば、 "helmet"という単語は4番目の遷移(mからe:helMEt)で除外できます。これは、テーブル内のそのエントリが偽であるためです。

最初の(hからaへの)移行は許可されていない(テーブルにも存在しない)ため、Wordハムスターは除外できます。

さて、あなたが排除しなかった残りのおそらく非常に少数の単語については、あなたが今それをしている方法で、あるいはここで他の答えのいくつかで示唆されるように実際にそれらをグリッドで見つけることを試みなさい。これは、グリッド内の同一文字間のジャンプから生じる誤検知を避けるためです。たとえば、単語 "help"はテーブルでは許可されていますが、グリッドでは許可されていません。

このアイディアに関するさらなるパフォーマンス改善のヒント:

  1. 2次元配列を使用する代わりに、1次元配列を使用して、2番目の文字のインデックスを自分で計算してください。そのため、上記のような12 x 12の配列ではなく、長さ144の1 Dの配列を作成します。すべての文字がグリッドに表示されない場合でも、常に同じアルファベットを使用します。辞書の単語と一致させるためにテストする必要がある、この1次元配列へのインデックスを事前計算することができます。たとえば、上記の例の 'hello'のインデックスは次のようになります。

    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. アイデアを3Dテーブル(1D配列として表現)に拡張します。つまり、すべて3文字の組み合わせが許可されます。こうすることで、さらに多くの単語をすぐに削除でき、各Wordの配列検索の数を1減らすことができます。 'hello'の場合、3つの配列検索(hel、ell、llo)だけが必要です。ちなみに、グリッドには3文字の移動が400回しかないため、このテーブルを作成するのは非常に簡単です。

  3. テーブルに含める必要があるグリッド内の移動のインデックスを事前計算します。上記の例では、以下のエントリを 'True'に設定する必要があります。

    (0,0) (0,1) -> here: h, b : [6][0]
    (0,0) (1,0) -> here: h, e : [6][3]
    (0,0) (1,1) -> here: h, e : [6][3]
    (0,1) (0,0) -> here: b, h : [0][6]
    (0,1) (0,2) -> here: b, c : [0][1]
    .
    :
    
  4. また、16個のエントリを持つ1-D配列でゲームグリッドを表し、3.で事前に計算されたテーブルにこの配列へのインデックスを含めます。

このアプローチを使えば、辞書を事前に計算してメモリにロード済みであれば、コードをめちゃくちゃ速く実行することができます。

ところで:あなたがゲームを構築している場合、やるべきもう一つのいいことは、バックグラウンドですぐにこれらの種類のことを実行することです。ユーザーがアプリのタイトル画面を見ながら「Play」を押す位置に指を入れたままで、最初のゲームの生成と解決を開始します。次に、ユーザーが前のゲームをプレイしながら次のゲームを生成して解決します。それはあなたのコードを実行するためにあなたに多くの時間を与えるはずです。

(私はこの問題が好きなので、実際にそれがどのように実行されるかを見るために私の提案をJavaでいつか実装しようと思うでしょう。私がしたら私はここにコードをポストするつもりです。)

更新:

さて、私は今日時間があり、このアイデアをJavaで実装しました:

class DictionaryEntry {
  public int[] letters;
  public int[] triplets;
}

class BoggleSolver {

  // Constants
  final int ALPHABET_SIZE = 5;  // up to 2^5 = 32 letters
  final int BOARD_SIZE    = 4;  // 4x4 board
  final int[] moves = {-BOARD_SIZE-1, -BOARD_SIZE, -BOARD_SIZE+1, 
                                  -1,                         +1,
                       +BOARD_SIZE-1, +BOARD_SIZE, +BOARD_SIZE+1};


  // Technically constant (calculated here for flexibility, but should be fixed)
  DictionaryEntry[] dictionary; // Processed Word list
  int maxWordLength = 0;
  int[] boardTripletIndices; // List of all 3-letter moves in board coordinates

  DictionaryEntry[] buildDictionary(String fileName) throws IOException {
    BufferedReader fileReader = new BufferedReader(new FileReader(fileName));
    String Word = fileReader.readLine();
    ArrayList<DictionaryEntry> result = new ArrayList<DictionaryEntry>();
    while (Word!=null) {
      if (Word.length()>=3) {
        Word = Word.toUpperCase();
        if (Word.length()>maxWordLength) maxWordLength = Word.length();
        DictionaryEntry entry = new DictionaryEntry();
        entry.letters  = new int[Word.length()  ];
        entry.triplets = new int[Word.length()-2];
        int i=0;
        for (char letter: Word.toCharArray()) {
          entry.letters[i] = (byte) letter - 65; // Convert ASCII to 0..25
          if (i>=2)
            entry.triplets[i-2] = (((entry.letters[i-2]  << ALPHABET_SIZE) +
                                     entry.letters[i-1]) << ALPHABET_SIZE) +
                                     entry.letters[i];
          i++;
        }
        result.add(entry);
      }
      Word = fileReader.readLine();
    }
    return result.toArray(new DictionaryEntry[result.size()]);
  }

  boolean isWrap(int a, int b) { // Checks if move a->b wraps board Edge (like 3->4)
    return Math.abs(a%BOARD_SIZE-b%BOARD_SIZE)>1;
  }

  int[] buildTripletIndices() {
    ArrayList<Integer> result = new ArrayList<Integer>();
    for (int a=0; a<BOARD_SIZE*BOARD_SIZE; a++)
      for (int bm: moves) {
        int b=a+bm;
        if ((b>=0) && (b<board.length) && !isWrap(a, b))
          for (int cm: moves) {
            int c=b+cm;
            if ((c>=0) && (c<board.length) && (c!=a) && !isWrap(b, c)) {
              result.add(a);
              result.add(b);
              result.add(c);
            }
          }
      }
    int[] result2 = new int[result.size()];
    int i=0;
    for (Integer r: result) result2[i++] = r;
    return result2;
  }


  // Variables that depend on the actual game layout
  int[] board = new int[BOARD_SIZE*BOARD_SIZE]; // Letters in board
  boolean[] possibleTriplets = new boolean[1 << (ALPHABET_SIZE*3)];

  DictionaryEntry[] candidateWords;
  int candidateCount;

  int[] usedBoardPositions;

  DictionaryEntry[] foundWords;
  int foundCount;

  void initializeBoard(String[] letters) {
    for (int row=0; row<BOARD_SIZE; row++)
      for (int col=0; col<BOARD_SIZE; col++)
        board[row*BOARD_SIZE + col] = (byte) letters[row].charAt(col) - 65;
  }

  void setPossibleTriplets() {
    Arrays.fill(possibleTriplets, false); // Reset list
    int i=0;
    while (i<boardTripletIndices.length) {
      int triplet = (((board[boardTripletIndices[i++]]  << ALPHABET_SIZE) +
                       board[boardTripletIndices[i++]]) << ALPHABET_SIZE) +
                       board[boardTripletIndices[i++]];
      possibleTriplets[triplet] = true; 
    }
  }

  void checkWordTriplets() {
    candidateCount = 0;
    for (DictionaryEntry entry: dictionary) {
      boolean ok = true;
      int len = entry.triplets.length;
      for (int t=0; (t<len) && ok; t++)
        ok = possibleTriplets[entry.triplets[t]];
      if (ok) candidateWords[candidateCount++] = entry;
    }
  }

  void checkWords() { // Can probably be optimized a lot
    foundCount = 0;
    for (int i=0; i<candidateCount; i++) {
      DictionaryEntry candidate = candidateWords[i];
      for (int j=0; j<board.length; j++)
        if (board[j]==candidate.letters[0]) { 
          usedBoardPositions[0] = j;
          if (checkNextLetters(candidate, 1, j)) {
            foundWords[foundCount++] = candidate;
            break;
          }
        }
    }
  }

  boolean checkNextLetters(DictionaryEntry candidate, int letter, int pos) {
    if (letter==candidate.letters.length) return true;
    int match = candidate.letters[letter];
    for (int move: moves) {
      int next=pos+move;
      if ((next>=0) && (next<board.length) && (board[next]==match) && !isWrap(pos, next)) {
        boolean ok = true;
        for (int i=0; (i<letter) && ok; i++)
          ok = usedBoardPositions[i]!=next;
        if (ok) {
          usedBoardPositions[letter] = next;
          if (checkNextLetters(candidate, letter+1, next)) return true;
        }
      }
    }   
    return false;
  }


  // Just some helper functions
  String formatTime(long start, long end, long repetitions) {
    long time = (end-start)/repetitions;
    return time/1000000 + "." + (time/100000) % 10 + "" + (time/10000) % 10 + "ms";
  }

  String getWord(DictionaryEntry entry) {
    char[] result = new char[entry.letters.length];
    int i=0;
    for (int letter: entry.letters)
      result[i++] = (char) (letter+97);
    return new String(result);
  }

  void run() throws IOException {
    long start = System.nanoTime();

    // The following can be pre-computed and should be replaced by constants
    dictionary = buildDictionary("C:/TWL06.txt");
    boardTripletIndices = buildTripletIndices();
    long precomputed = System.nanoTime();


    // The following only needs to run once at the beginning of the program
    candidateWords     = new DictionaryEntry[dictionary.length]; // WAAAY too generous
    foundWords         = new DictionaryEntry[dictionary.length]; // WAAAY too generous
    usedBoardPositions = new int[maxWordLength];
    long initialized = System.nanoTime(); 

    for (int n=1; n<=100; n++) {
      // The following needs to run again for every new board
      initializeBoard(new String[] {"DGHI",
                                    "KLPS",
                                    "YEUT",
                                    "EORN"});
      setPossibleTriplets();
      checkWordTriplets();
      checkWords();
    }
    long solved = System.nanoTime();


    // Print out result and statistics
    System.out.println("Precomputation finished in " + formatTime(start, precomputed, 1)+":");
    System.out.println("  Words in the dictionary: "+dictionary.length);
    System.out.println("  Longest Word:            "+maxWordLength+" letters");
    System.out.println("  Number of triplet-moves: "+boardTripletIndices.length/3);
    System.out.println();

    System.out.println("Initialization finished in " + formatTime(precomputed, initialized, 1));
    System.out.println();

    System.out.println("Board solved in "+formatTime(initialized, solved, 100)+":");
    System.out.println("  Number of candidates: "+candidateCount);
    System.out.println("  Number of actual words: "+foundCount);
    System.out.println();

    System.out.println("Words found:");
    int w=0;
    System.out.print("  ");
    for (int i=0; i<foundCount; i++) {
      System.out.print(getWord(foundWords[i]));
      w++;
      if (w==10) {
        w=0;
        System.out.println(); System.out.print("  ");
      } else
        if (i<foundCount-1) System.out.print(", ");
    }
    System.out.println();
  }

  public static void main(String[] args) throws IOException {
    new BoggleSolver().run();
  }
}

これがいくつかの結果です:

元の質問(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

元の質問の例として投稿された手紙の場合(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

次の5×5グリッドの場合:

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

それはこれを与えます:

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

このために、私は TWL06トーナメントスクラブルワードリスト を使用しました。元の質問のリンクが機能しなくなったためです。このファイルは1.85MBなので、少し短くなっています。そしてbuildDictionary関数は3文字以下のすべての単語を捨てます。

これがこのパフォーマンスについていくつかの観察です。

  • これは、報告されているVictor NicolletのOCaml実装のパフォーマンスよりも約10倍遅いです。これが異なるアルゴリズム、彼が使った短い辞書、彼のコードがコンパイルされて私のJava仮想マシンで動いているという事実、あるいは私たちのコンピュータのパフォーマンス(私のはWinXPを動かしているIntel Q6600 @ 2.4MHz)のせいです。知りません。しかし、それは最初の質問の終わりに引用された他の実装のための結果よりはるかに速いです。そのため、このアルゴリズムがトライ辞書より優れているかどうかにかかわらず、現時点ではわかりません。

  • checkWordTriplets()で使用されているテーブルメソッドは実際の答えに非常に良い近似をもたらします。渡された3-5ワードのうち1ワードのみがcheckWords()テストに失敗します(候補数実際のワード数を参照)上記)。

  • 上で見ることができないもの:checkWordTriplets()関数は約3.65msかかり、それゆえ検索プロセスにおいて完全に支配的です。 checkWords()関数は、残りの0.05-0.20ミリ秒をほぼ占有します。

  • checkWordTriplets()関数の実行時間は辞書のサイズに比例しており、実質的にボードサイズとは無関係です!

  • checkWords()の実行時間は、ボードサイズとcheckWordTriplets()によって除外されないワード数によって異なります。

  • 上記のcheckWords()の実装は、私が思いついた最も最初のバージョンです。基本的にまったく最適化されていません。 checkWordTriplets()と比べると、アプリケーションの全体的なパフォーマンスには無関係なので、私は心配しませんでした。 しかし、ボードサイズが大きくなると、この関数は次第に遅くなり、やがて問題になります。そして、それも同様に最適化する必要があります。

  • このコードのいいところは、その柔軟性です。

    • ボードサイズを簡単に変更できます。10行目とinitializeBoard()に渡されたString配列を更新します。
    • それはより大きな/異なるアルファベットをサポートすることができ、パフォーマンスのオーバーヘッドなしに 'Qu'を1文字として扱うことのようなことを扱うことができます。これを行うには、9行目と、文字が数字に変換される場所をいくつか更新する必要があります(現在のところ、ASCII値から65を引くことによって)

わかりました、しかし私は今ここにこの記事が十分長いwaaaayであると思う。私はあなたがするかもしれないどんな質問にでも確実に答えることができます、しかしそれをコメントに動かしましょう。

23
Markus A.

私のJavaでの試みファイルを読んでトライするのに約2秒かかり、パズルを解くのに約50ミリ秒かかります。質問にリンクされている辞書を使いました(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

ソースコードは6つのクラスで構成されています。私はそれらを以下に投稿するつもりです(これがStackOverflowの正しいプラクティスではない場合は、教えてください)。

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

驚くべきことに、誰もこれのPHPバージョンを試みませんでした。

これはJohn FouhyのPythonソリューションの実用的なPHPバージョンです。

私は他のみんなの答えからいくつかの指針を取りましたが、これはほとんどジョンからコピーされました。

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

試してみたいのであれば、 ライブリンク です。私のローカルマシンでは2秒かかりますが、ウェブサーバーでは5秒かかります。どちらの場合も、それほど速くはありません。それでも、それでも、それは非常に恐ろしいので、時間を大幅に減らすことができると想像することができます。それを達成する方法についてのどんなポインターでも評価されるでしょう。 PHPのタプルの欠如は、その座標を扱うのが奇妙になり、地獄が起こっていることだけを理解することができなかったため、まったく役に立ちませんでした。

EDIT:いくつか修正した結果、ローカルで1秒以下になりました。

19

VBに興味がありませんか? :)私は抵抗できなかった。私はここで提示された解決策の多くとは異なる方法でこれを解決しました。

私の時代は:

  • ハッシュテーブルに辞書とWordのプレフィックスをロードする:0.5〜1秒。
  • 単語を見つけること:10ミリ秒以下の平均.

編集:Webホストサーバー上の辞書の読み込み時間は私の自宅のコンピューターよりも約1〜1.5秒長く実行されています。

サーバーの負荷によってどれだけ悪化するのかわからない。

私は自分の解決策を.NetのWebページとして書いた。 myvrad.com/boggle

元の質問で参照されている辞書を使用しています。

手紙はWordで再利用されていません。 3文字以上の単語のみが見つかります。

トライではなく、すべての一意のWordプレフィックスと単語のハッシュテーブルを使用しています。トライについては知りませんでしたので、そこで何かを学びました。完全な単語に加えて単語の接頭辞のリストを作成するというアイデアは、ついに私の時間をかなりの数に減らしたものです。

詳細についてはコードのコメントを読んでください。

これがコードです:

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
16
rvarcher

問題の声明を見た途端、私は "Trie"と思った。しかし、他のいくつかのポスターがそのアプローチを利用しているのを見て、私はただ違うという別のアプローチを探しました。残念なことに、トライのアプローチはより良いパフォーマンスを発揮します。私は自分のマシンでKentのPerlソリューションを実行しましたが、辞書ファイルを使用するように調整した後、実行に.31秒かかりました。私自身のPerlの実装では.54秒が必要です。

これが私のアプローチでした。

  1. 合法的な移行をモデル化するために移行ハッシュを作成します。

  2. 16 ^ 3の可能な3文字の組み合わせすべてを繰り返します。

    • ループ内で、違法な移行を除外し、同じ広場への訪問を繰り返します。すべての有効な3文字のシーケンスを作成し、それらをハッシュに格納します。
  3. その後、辞書内のすべての単語をループします。

    • 長すぎたり短すぎたりする単語を除外する
    • 各単語を横切って3文字のウィンドウをスライドさせ、それがステップ2からの3文字のコンボの中にあるかどうかを確認します。失敗した単語を除外します。これにより、ほとんどの不一致が排除されます。
    • それでも解消されない場合は、再帰的アルゴリズムを使用して、パズルを通過することによって単語を形成できるかどうかを確認します。 (この部分は遅いですが、まれにしか呼ばれません。)
  4. 見つけた言葉をプリントアウトする。

    私は3文字と4文字のシーケンスを試しましたが、4文字のシーケンスはプログラムを遅くしました。

私のコードでは、辞書に/ usr/share/dict/wordsを使用しています。それはMAC OS Xと多くのUnixシステムで標準的になります。必要に応じて他のファイルを使用することができます。別のパズルを解読するには、変数@puzzleを変更するだけです。これはより大きな行列に適応するのは簡単でしょう。あなたはちょうど%transitionハッシュと%legalTransitionsハッシュを変更する必要があるでしょう。

このソリューションの長所は、コードが短く、データ構造が単純なことです。

これがPerlコードです(あまりにも多くのグローバル変数を使用しています、私は知っています):

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

私は超遅刻していることを知っていますが、私はしばらく前にPHPでこれらのうちの1つを作りました - ちょうど楽しみのためにも...

http://www.lostsockdesign.com.au/sandbox/boggle/index.php?letters=fxieamloewbxastで0.90108秒に75ワード(133 pts)が見つかりました

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

プログラムが実際に何をしているのかを示します。各文字は、それぞれの「。」の間にパターンを調べ始めるところです。試みた経路を示しています。もっと '。'さらに検索したところあります。

あなたがコードが欲しいかどうか私に知らせてください…それはPHPとHTMLの恐ろしい組み合わせで、日の目を見ることを決して意味していなかったので、ここに投稿しません:P

9
Danny

私は3つの月を使って10の最高点密度5 x 5 Boggleボード問題の解決に取り組んだ。

この問題は解決され、5つのWebページに完全に開示されました。質問を私に連絡してください。

ボード分析アルゴリズムは、明示的なスタックを使用して、直接的な子情報を持つ有向非巡回単語グラフとタイムスタンプ追跡メカニズムを介してボードの四角形を擬似的に再帰的に走査します。これは世界で最も先進的なLexiconデータ構造である可能性が非常に高いです。

この方式は、クアッドコア上で毎秒約10,000枚の非常に優れたボードを評価します。 (9500+ポイント)

親Webページ:

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

コンポーネントWebページ:

最適スコアボード - http://www.pathcom.com/~vadco/binary.html

高度な語彙体系 - http://www.pathcom.com/~vadco/adtdawg.html

ボード解析アルゴリズム - http://www.pathcom.com/~vadco/guns.html

並列バッチ処理 - http://www.pathcom.com/~vadco/parallel.html

- この包括的な作業体系は、最高のものを要求している人にのみ役立ちます。

9

まず、C#言語デザイナーの1人が関連する問題をどのように解決したかを読んでください。 http://blogs.msdn.com/ericlippert/archive/2009/02/04/a-nasality-talisman-for-the-sultana -analyst.aspx .

彼と同様に、アルファベット順にソートされた一連の文字からそれらの文字からつづることができる単語のリストから辞書を作成することによって、辞書と単語を正規化することから始めることができます。

次に、掲示板から可能な単語を作成し、それらを調べます。それはあなたをかなり遠くへ連れて行くだろうと私は思うが、物事をスピードアップするかもしれない確かにより多くのトリックがある。

4
RossFabricant

あなたの検索アルゴリズムはあなたの検索が続くにつれて絶えず単語リストを減らしますか?

例えば、上記の検索では、あなたの単語が始まることができるのは13文字だけです(事実上、開始文字の半分の数に減らされます)。

文字の並べ替えを追加すると、使用可能なWordセットがさらに減少し、必要な検索が減少します。

私はそこから始めたいと思います。

4
jerebear

私は完全な解決策についてもっと考えなければならないでしょう、しかし便利な最適化として、それがすべてに基づいてdigramsとtrigramの頻度の表(2文字と3文字の組み合わせ)を事前計算する価値があるかどうか疑問に思います。あなたの辞書からの単語、そしてあなたの検索に優先順位をつけるのにこれを使用しなさい。私は言葉の始まりの文字と一緒に行きたいと思います。そのため、辞書に "India"、 "Water"、 "Extreme"、および "Extraordinary"という単語が含まれている場合、事前に計算されたテーブルは次のようになります。

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

次に、これらのジグラムを共通性の順に検索します(最初にEX、次にWA/IN)。

4
Smashery

私は言葉に基づいて手紙の木を作ることを提案します。木はこのように文字構造体で構成されます。

letter: char
isWord: boolean

それから木を作り、深さごとに新しい文字を追加します。言い換えれば、最初のレベルにはアルファベットがあります。それからそれらの木のそれぞれから、あなたがすべての単語を綴るまで、さらに別の26のエントリなどがあるでしょう。この解析済みツリーにハングすると、すべての可能な答えが早く検索されます。

この解析木を使えば、非常に素早く解決策を見つけることができます。これが擬似コードです。

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.

これはちょっとした動的計画法で加速することができます。たとえば、あなたのサンプルでは、​​2つの「A」は両方とも「E」と「W」の隣にあります。このためのコードを実際に詳しく説明するのに十分な時間がありませんが、アイデアを集めることができると思います。

また、あなたが "Boggle solver"のためにあなたがGoogleであるならば、私はあなたが他の解決策を見つけると確信しています。

4
Daniel Lew

楽しみのために、bashで実装しました。超高速ではありませんが、合理的です。

http://dev.xkyle.com/bashboggle/

4
Kyle

私は自分のソルバーをC++で書きました。私はカスタムツリー構造を実装しました。私はそれがトライと見なすことができるかどうかわからないが、それは似ています。各ノードには26の枝があり、アルファベットの各文字に1つずつです。私は自分の辞書の枝と平行して、ゴーグルボードの枝を横切ります。ブランチが辞書に存在しない場合、私はBoggleボードでそれを探すのをやめます。私はボード上のすべての文字を整数に変換します。それで、 'A' = 0です。それは単なる配列なので、検索は常にO(1)です。各ノードは、単語を完成したかどうか、およびその子にいくつ単語が存在するかを格納します。単語が同じ単語を繰り返し検索することを減らすために見つけられるので木は剪定される。私は剪定もO(1)であると思います。

CPU:Pentium SU2700 1.3 GHz
RAM:3GB

1秒以内に178,590ワードの辞書をロードします。
100×100のBoggle(boggle.txt)を4秒で解決します。 〜44,000語の単語が見つかりました。
4x4のゴーグルを解くのは速すぎて意味のあるベンチマークを提供することができません。 :)

ファストボグルソルバーGitHubレポ

3
Will

陽気です。私は数日前に同じ質問をしたので同じ質問を投稿しました。グーグルで boggle solver python を検索して欲しい答えをすべて手に入れたので、私はそうしなかった。

3
physicsmichael

私はこの質問の時が来てなくなったことを実感します、しかし私は自分自身でソルバーに取り組んでいて、そしてグーグルしながらこれにつまずいたので、私はそれが他のものと少し違うように私に言及するべきです。

私はゲームボード用のフラットアレイを使用し、有効な隣人から有効な隣人まで横断しながら、ボード上の各文字から再帰的な探索を行い、現在の文字リストがインデックス内の有効な接頭辞であればハントを拡張しました。現在のWordの概念を横断するのは、Wordを構成する文字ではなく、掲示板へのインデックスのリストです。インデックスをチェックするとき、インデックスは文字に変換され、チェックが行われます。

インデックスは、ちょっとトライに似た総当たり辞書ですが、インデックスのPythonicクエリを可能にします。単語「cat」と「cater」がリストに含まれている場合は、これを辞書に入れます。

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

したがって、current_Wordが「ca」の場合、'ca' in dはTrueを返すので、これは有効な接頭辞であることがわかります(ボードのトラバースを続けます)。そしてcurrent_Wordが 'cat'であれば、それは有効な接頭辞であり'cat' in d['cat']もTrueを返すので有効なWordであることがわかります。

これが遅すぎるとは思えない読み取り可能なコードを許可していると感じた場合。他の人と同じように、このシステムの経費は索引を読む/構築することです。ボードを解くのはかなりノイズです。

コードは http://Gist.github.com/268079 です。私はこの問題をたくさんの魔法や不明瞭さに悩まさずに理解したかったので、それは意図的に垂直で素朴です。

3
cdent

N行M列のBoggleボードを考えて、次のように仮定しましょう。

  • N * Mは可能な単語の数よりもかなり大きい
  • N * Mは、可能な限り最長のWordよりかなり大きい

これらの仮定の下で、この解決法の複雑さはO(N * M)です。

この1つのサンプルボードの実行時間をさまざまな方法で比較しても問題はありませんが、完全を期すために、この解決法は私の最新のMacBook Proでは<0.2秒で完了します。

この解決策は、コーパス内の各Wordに対してすべての可能なパスを見つけます。

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

私は OCamlでソリューションを実装しました を持っています。辞書をトライとしてプリコンパイルし、2文字のシーケンス頻度を使用して、Wordには決して現れないエッジを排除し、処理をさらに高速化します。

それは0.35msであなたの例のボードを解決します(メモリにトライをロードすることに主に関連している追加の6msの起動時間で)。

解決策は次のとおりです。

["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"]
1
Victor Nicollet

私はパーティーが本当に遅刻していることを知っていますが、私はコーディングの練習として、いくつかのプログラミング言語(C++、Java、Go、C#、Python、Ruby、JavaScript、Julia、Lua、PHP、Perl)でゴーグルソルバーを実装しました。私は誰かがそれらに興味があるかもしれないと思ったので、私はここにリンクを残します: https://github.com/AmokHuginnsson/boggle-solvers

1
AmokHuginnsson

それで私はこれを解決するための別のPHP方法を追加したいと思いました。辞書ファイルに対して正規表現の一致を使用するなど、やりたいことが少しありますが、今は辞書ファイル全体をwordListにロードしています。

リンクリストのアイデアを使用してこれを行いました。各ノードは、文字値、位置値、および次のポインタを持ちます。

場所の値は、2つのノードが接続されているかどうかを確認する方法です。

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

そのグリッドを使用して、最初のノードの位置が同じ行の2番目のノードの位置+/- 1、上下の行の+/- 9、10、11に等しい場合、2つのノードが接続されていることがわかります。

メインサーチには再帰を使います。これはwordListからWordを取り出し、すべての可能性のある開始点を見つけ、それから次の可能性のある接続を再帰的に見つけます。それはすでに使用している場所には行けないことを念頭に置いています。

とにかく、私はそれがいくらかのリファクタリングを必要とすることを知っていて、そしてそれをよりきれいにする方法についての考えを聞きたいですが、それは私が使っている辞書ファイルに基づいて正しい結果を生み出します。ボード上の母音の数と組み合わせにもよりますが、約3〜6秒かかります。辞書の結果をpreg_matchすると、それは大幅に減少することを私は知っています。

<?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
Nate

これがNLTKツールキットで定義済みの単語を使用する解決策です。NLTKにはnltk.corpusパッケージがあり、単語と呼ばれるパッケージがあり、2Lakhsを超える英語の単語が含まれています。

行列を作成したら、それを文字配列に変換して次のコードを実行します。

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)

出力:

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

あなたがそれを手に入れられることを願っています。

1
lava kumar

Node.JSのJavaScriptソリューション。辞書ファイルを読むことを含む1秒以内にすべての100のユニークな単語を計算します(MBA 2012)。

出力:
["FAM"、 "Tux"、 "TUB"、 "FAE"、 "ELI"、 "Elm"、 "ELB"、 "TWA"、 "TWA"、 "SAW"、 "AMI"、 "SWA"、 "SWA"、 "AME"、 "SEA"、 "AES"、 "AWL"、 "AWE"、 "SEA"、 "AWA"、 "MIX"、 "MIL"、 "AST" "、" ASE "、" MAX "、" MAE "、" MEW "、" AWE "、" MES "、" AWL "、" LIE "、" LIM "、" AWA "、" AES "、 "BUT"、 "BLO"、 "WAS"、 "WAE"、 "LEI"、 "LEO"、 "LOB"、 "LOX"、 "WEM"、 "OIL"、 "WEA"、 "WEA" "、" WAE "、" WAX "、" WAF "、" MILO "、" WAME "、" TWAS "、" TWAE "、" EMIL "、" WEAM "、" OIME "、" AXIL "、 「WEST」、「TWAE」、「LIMB」、「WASE」、「BLEO」、「STUB」、「BOIL」、「BOLE」、「Lime」、「SAWT」、「LIMA」、「MESA」 "、" MEWL "、" AXLE "、" FAME "、" ASEM "、" AMIL "、" SEAX "、" SEAM "、" SEMI "、" SEMI "、" AMBO "、" AMLI "、 "AXILE"、 "AMBLE"、 "SWAMI"、 "AWEST"、 "LIMEST"、 "LIMES"、 "LIMBU"、 "LIMBO"、 "EMBOX"、 "SEMBLE"、 "EMBOLE"、 "WAMBLE" "、" FAMBLE "]

コード:

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

単純なソートと辞書での二分検索の使用はどうですか?

リスト全体を.35秒で返し、さらに最適化できます(たとえば、未使用の文字を含む単語を削除するなど)。

from bisect import bisect_left

f = open("dict.txt")
D.extend([line.strip() for line in f.readlines()])
D = sorted(D)

def neibs(M,x,y):
    n = len(M)
    for i in xrange(-1,2):
        for j in xrange(-1,2):
            if (i == 0 and j == 0) or (x + i < 0 or x + i >= n or y + j < 0 or y + j >= n):
                continue
            yield (x + i, y + j)

def findWords(M,D,x,y,prefix):
    prefix = prefix + M[x][y]

    # find Word in dict by binary search
    found = bisect_left(D,prefix)

    # if found then yield
    if D[found] == prefix: 
        yield prefix

    # if what we found is not even a prefix then return
    # (there is no point in going further)
    if len(D[found]) < len(prefix) or D[found][:len(prefix)] != prefix:
        return

    # recourse
    for neib in neibs(M,x,y):
        for Word in findWords(M,D,neib[0], neib[1], prefix):
            yield Word

def solve(M,D):
    # check each starting point
    for x in xrange(0,len(M)):
        for y in xrange(0,len(M)):
            for Word in findWords(M,D,x,y,""):
                yield Word

grid = "fxie amlo ewbx astu".split()
print [x for x in solve(grid,D)]
0

私はこれをcで解決しました。私のマシンで実行するのに約48ミリ秒かかります(ディスクから辞書をロードしてトライを作成するのに費やした時間の約98%)。辞書は/ usr/share/dict/american-englishで、62886ワードです。

ソースコード

0
matzahboy

この解は与えられた掲示板で検索する方向も与えます

アルゴ:

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

出力:

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

コード:

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 __== '__main__':
    main(trie_node)
0
naren

私はこれを完璧にそして非常に速く解決しました。私はそれをAndroidアプリに入れました。 Playストアのリンクでビデオを見て、実際に動作していることを確認します。

Wordの攻略は、任意のマトリックススタイルのWordゲームを "クラック"するアプリです。このアプリは私がWordのスクランブラでカンニングするのを手助けするために作られました。これは、ワード検索、ルズル、ワード、ワードファインダ、ワードクラック、ボグルなどに使用できます。

ここで見ることができます https://play.google.com/store/apps/details?id=com.harris.wordcracker

ビデオで動作中のアプリを表示する https://www.youtube.com/watch?v=DL2974WmNAI

0
Josh Harris

私もこれをJavaで解決しました。私の実装は269行の長さで、とても使いやすいです。まず、Bogglerクラスの新しいインスタンスを作成してから、グリッドをパラメータとして使用してsolve関数を呼び出す必要があります。私のコンピュータに50 000語の辞書をロードするのに約100ミリ秒かかり、それは約10-20ミリ秒の間に単語を見つけます。見つかった単語は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
MikkoP
    package ProblemSolving;

import Java.util.HashSet;
import Java.util.Set;

/**
 * Given a 2-dimensional array of characters and a
 * dictionary in which a Word can be searched in O(1) time.
 * Need to print all the words from array which are present
 * in dictionary. Word can be formed in any direction but
 * has to end at any Edge of array.
 * (Need not worry much about the dictionary)
 */
public class DictionaryWord {
    private static char[][] matrix = new char[][]{
            {'a', 'f', 'h', 'u', 'n'},
            {'e', 't', 'a', 'i', 'r'},
            {'a', 'e', 'g', 'g', 'o'},
            {'t', 'r', 'm', 'l', 'p'}
    };
    private static int dim_x = matrix.length;
    private static int dim_y = matrix[matrix.length -1].length;
    private static Set<String> wordSet = new HashSet<String>();

    public static void main(String[] args) {
        //dictionary
        wordSet.add("after");
        wordSet.add("hate");
        wordSet.add("hair");
        wordSet.add("air");
        wordSet.add("eat");
        wordSet.add("tea");

        for (int x = 0; x < dim_x; x++) {
            for (int y = 0; y < dim_y; y++) {
                checkAndPrint(matrix[x][y] + "");
                int[][] visitedMap = new int[dim_x][dim_y];
                visitedMap[x][y] = 1;
                recursion(matrix[x][y] + "", visitedMap, x, y);
            }
        }
    }

    private static void checkAndPrint(String Word) {
        if (wordSet.contains(Word)) {
            System.out.println(Word);
        }
    }

    private static void recursion(String Word, int[][] visitedMap, int x, int y) {
        for (int i = Math.max(x - 1, 0); i < Math.min(x + 2, dim_x); i++) {
            for (int j = Math.max(y - 1, 0); j < Math.min(y + 2, dim_y); j++) {
                if (visitedMap[i][j] == 1) {
                    continue;
                } else {
                    int[][] newVisitedMap = new int[dim_x][dim_y];
                    for (int p = 0; p < dim_x; p++) {
                        for (int q = 0; q < dim_y; q++) {
                           newVisitedMap[p][q] = visitedMap[p][q];
                        }
                    }
                    newVisitedMap[i][j] = 1;
                    checkAndPrint(Word + matrix[i][j]);
                    recursion(Word + matrix[i][j], newVisitedMap, i, j);
                }
            }
        }
    }

}
0
Dheeraj Sachan
import Java.util.HashSet;
import Java.util.Set;

/**
 * @author Sujeet Kumar ([email protected]) It prints out all strings that can
 *         be formed by moving left, right, up, down, or diagonally and exist in
 *         a given dictionary , without repeating any cell. Assumes words are
 *         comprised of lower case letters. Currently prints words as many times
 *         as they appear, not just once. *
 */

public class BoggleGame 
{
  /* A sample 4X4 board/2D matrix */
  private static char[][] board = { { 's', 'a', 's', 'g' },
                                  { 'a', 'u', 't', 'h' }, 
                                  { 'r', 't', 'j', 'e' },
                                  { 'k', 'a', 'h', 'e' }
};

/* A sample dictionary which contains unique collection of words */
private static Set<String> dictionary = new HashSet<String>();

private static boolean[][] visited = new boolean[board.length][board[0].length];

public static void main(String[] arg) {
    dictionary.add("sujeet");
    dictionary.add("sarthak");
    findWords();

}

// show all words, starting from each possible starting place
private static void findWords() {
    for (int i = 0; i < board.length; i++) {
        for (int j = 0; j < board[i].length; j++) {
            StringBuffer buffer = new StringBuffer();
            dfs(i, j, buffer);
        }

    }

}

// run depth first search starting at cell (i, j)
private static void dfs(int i, int j, StringBuffer buffer) {
    /*
     * base case: just return in recursive call when index goes out of the
     * size of matrix dimension
     */
    if (i < 0 || j < 0 || i > board.length - 1 || j > board[i].length - 1) {
        return;
    }

    /*
     * base case: to return in recursive call when given cell is already
     * visited in a given string of Word
     */
    if (visited[i][j] == true) { // can't visit a cell more than once
        return;
    }

    // not to allow a cell to reuse
    visited[i][j] = true;

    // combining cell character with other visited cells characters to form
    // Word a potential Word which may exist in dictionary
    buffer.append(board[i][j]);

    // found a Word in dictionary. Print it.
    if (dictionary.contains(buffer.toString())) {
        System.out.println(buffer);
    }

    /*
     * consider all neighbors.For a given cell considering all adjacent
     * cells in horizontal, vertical and diagonal direction
     */
    for (int k = i - 1; k <= i + 1; k++) {
        for (int l = j - 1; l <= j + 1; l++) {
            dfs(k, l, buffer);

        }

    }
    buffer.deleteCharAt(buffer.length() - 1);
    visited[i][j] = false;
  }
}
0
Sujeet

DFAアルゴリズムを使用して、C#でこれを解決しました。あなたは私のコードをチェックアウトすることができます

https://github.com/attilabicsko/wordshuffler/

マトリックス内の単語を見つけることに加えて、私のアルゴリズムは単語の実際のパスを保存するので、Word Finderゲームを設計するために、実際のパスにWordがあるかどうかをチェックできます。

0
Attila Bicskó

これが私のJava実装です: https://github.com/zouzhile/interview/blob/master/src/com/interview/algorithms/tree/BoggleSolver.Java

トライビルドには0時間0分1秒532ミリ秒かかりました
単語検索には0時間0分0秒92ミリ秒かかりました

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 

注:このスレッドの冒頭で辞書と文字マトリックスを使用しました。コードは私のMacBookProで実行されました、下記はマシンに関するいくつかの情報です。

モデル名:MacBook Pro
モデルID:MacBookPro 8、1
プロセッサー名:Intel Core i5
プロセッサ速度:2.3 GHz
プロセッサー数:1
コアの総数:2
L2キャッシュ(1コアあたり):256 KB
L3キャッシュ:3 MB
メモリ:4 GB
Boot ROMバージョン:MBP81.0047.B0E
SMCのバージョン(システム):1.68f96

0
Zhile Zou

これが私が思いつきの問題を解決するために思いついた解決策です。私はそれが物事を行うための最も "Pythonic"の方法だと思います:

from itertools import combinations
from itertools import izip
from math import fabs

def isAllowedStep(current,step,length,doubleLength):
            # for step == length -1 not to be 0 => trivial solutions are not allowed
    return length > 1 and \
           current + step < doubleLength and current - step > 0 and \
           ( step == 1 or step == -1 or step <= length+1 or step >= length - 1)

def getPairwiseList(someList):
    iterableList = iter(someList)
    return izip(iterableList, iterableList)

def isCombinationAllowed(combination,length,doubleLength):

    for (first,second) in  getPairwiseList(combination):
        _, firstCoordinate = first
        _, secondCoordinate = second
        if not isAllowedStep(firstCoordinate, fabs(secondCoordinate-firstCoordinate),length,doubleLength):
            return False
    return True

def extractSolution(combinations):
    return ["".join([x[0] for x in combinationTuple]) for combinationTuple in combinations]


length = 4
text = Tuple("".join("fxie amlo ewbx astu".split()))
textIndices = Tuple(range(len(text)))
coordinates = Zip(text,textIndices)

validCombinations = [combination for combination in combinations(coordinates,length) if isCombinationAllowed(combination,length,length*length)]
solution = extractSolution(validCombinations)

この部分では、可能な限りすべての一致に使用しないことをお勧めしますしかし、生成した単語が実際に有効な単語を構成しているかどうかを確認する可能性があります。

import mechanize
def checkWord(Word):
    url = "https://en.oxforddictionaries.com/search?filter=dictionary&query="+Word
    br = mechanize.Browser()
    br.set_handle_robots(False)
    response = br.open(url)
    text = response.read()
    return "no exact matches"  not in text.lower()

print [valid for valid in solution[:10] if checkWord(valid)]