如何从字母矩阵中找到可能的单词列表[Boggle Solver]


376

最近,我一直在iPhone上玩一个名为Scramble的游戏。你们中有些人可能将此游戏称为Boggle。本质上,当游戏开始时,您会得到一个字母矩阵,如下所示:

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

游戏的目标是找到尽可能多的单词,这些单词可以通过将字母链接在一起来形成。您可以从任何字母开始,并且包围它的所有字母都是公平的游戏,然后转到下一个字母时,包围该字母的所有字母都是公平的游戏,除了任何以前使用的字母。因此在上面的网格,例如,我能想出的话LOBTUXSEAFAME,等词必须至少有3个字符,并且不超过N×N个字符,这将是本场比赛16,但可以在一些实现改变。尽管这款游戏既有趣又令人上瘾,但我显然并不擅长于此,我想通过编写一个程序来欺骗我一点,该程序可以给我最好的单词(单词越长,您获得的积分就越多)。

样本虫
(来源:boggled.org

不幸的是,我对算法或其效率等不是很好。我的第一次尝试是使用诸如此类的字典(〜2.3MB),并进行线性搜索以尝试将组合与字典条目匹配。这需要非常长时间才能找到可能的单词,并且由于您每轮只有2分钟的时间,因此这根本不够用。

我很想看看是否有任何Stackoverflowers可以提出更有效的解决方案。我主要在寻找使用Big 3 P的解决方案:Python,PHP和Perl,尽管Java或C ++的任何功能也很酷,因为速度是必不可少的。

当前解决方案

  • 亚当·罗森菲尔德(Adam Rosenfield),Python,约20秒
  • John Fouhy,Python,大约3秒
  • Kent Fredric,Perl,〜1秒
  • Darius Bacon,Python,〜1秒
  • rvarcher,VB.NET (实时链接)
  • Paolo Bergantino,PHP (实时链接),〜5s(本地〜2s)

18
功能需求MOAR拼图
Kent Fredric

6
关于时间:在我的解决方案中,几乎所有时间都花在了构建Trie上。一旦构建了特里树,它可以重复使用很多次。如果仅解决一个难题,则使用更简单的数据结构(例如一组所有单词和所有前缀)会更有效率。
亚当·罗森菲尔德

3
而且,亚当的字典更大,这可以从他的解决方案使用的较长单词的数量中得到证明。它们都应基于通用词典进行测试。
Rich Bradshaw

2
我猜没人会玩Boggle吗?“ Qu”是一个“字母”,我不确定有多少解决方案抓住了这个小细节。看起来其中一些问题使您可以独立使用“ u”,还有其他问题。
Qsario

2
我最近有这个问题作为面试问题,并且深陷其中。我将其视为图形问题,这很好,但是这里的解决方案使用的空间要少得多。我现在正在编写自己的解决方案。对所有贡献者做的很好!
Peter Friend

Answers:


143

我的答案与此处的其他答案一样,但我将其发布,因为它比设置其他字典更快,因此看起来比其他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中执行相同的操作可使速度提高一倍。


该程序只给我一个字。怎么来的?
Paolo Bergantino,

我不想淹没在输出中。请参阅底部的评论。
Darius Bacon

6
或获取所有没有路径的单词:print''.join(sorted(set(word(solve()中的(word,path)的单词))))
Darius Bacon

2
很多时间只花在解析字典上。我将其预解析为一个“ wordlines.py”文件,该文件只是一个列表,每个单词都是一个元素。因为它是一个.py文件,所以它将变成一个.pyc文件。因此,我将其导入,而不是read()。splitlines()。这样,在我的盒子上,我将在十分之一秒的时间内解决它。
肖恩·赖夫施奈德

1
@shellscape,它是Python 2代码。Python 3放弃了解构参数的功能,例如def neighbors((x, y))(据我所知毫无意义)。此外,还需要在的参数周围加上括号print
Darius Bacon

116

您将获得的最快解决方案可能涉及将字典存储在trie中。然后,创建一个三元组(xys)队列,其中队列中的每个元素都对应一个单词的前缀s,可以在网格中拼写该单词的前缀,以位置(xy)结尾。用N x N个元素(其中N是网格的大小)初始化队列,网格中的每个正方形都包含一个元素。然后,算法进行如下:

当队列不为空时:
  使三元组(x,y,s)出队
  对于每个字母(x,y)相邻的正方形(x',y'):
    如果s + c是一个单词,则输出s + c
    如果s + c是单词的前缀,请在队列中插入(x',y',s + c)

如果将字典存储在trie中,则可以在固定时间内测试s + c是单词还是单词的前缀(前提是您还要在每个队列数据中保留一些额外的元数据,例如指向当前节点的指针)因此,此算法的运行时间为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”,“榆树'',``榆树'',``阿米'',``阿玛'',``阿姆'',``阿伊斯'',``阿勒尔'',``阿瓦'',``阿威'',``阿瓦'',``混合'',``妈妈'',``米尔'' ,'mam','max','mae','maw','mew','mem','mes','lob','lox','lei','leo','lie','lim','oil','olm','ewe','eme','wax','waf','wae','waw','wem' ,'wea','wea','was','waw','wae','bob','blo','bub','but','ast','ase','asa','锥子'',``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”,“ mima”,“ mime”,“ milo”,“英里”,“猫”,“中”,“中”,“ lolo”,“ lobo”,“ lima”,“石灰”,“ limb”,“ lile”,“ oime”,“ oleo”,“ olio” ,“ oboe”,“ obol”,“ emim”,“ emil”,“ east”,“ ease”,“ wame”,“ wawa”,“ wawa”,“ weam”,“ west”,“ wese”,“ wast”,“ wase”,“ wawa”,“ wawa”,“ boil”,“ bolo”,“ bole”,“ bobo”,“ blob”,“ bleo”,“ bubo”,“ asem”,“ stub” ,“ stut”,“ swam”,“ semi”,“ seme”,“ seam”,“ seax”,“ sasa”,“ sawt”,“ tutu”,“ tuts”,“ twae”,“ twas”,“ twae”,“ ilima”,“ amble”,“ axile”,“西南”,“妈妈”,“曼波”,“马克西姆”,“ mease”,“ mesem”,“ limax”,“石灰”,“ limbo”,“ limbu”,“ obole”,“ emesa”,“ embox”,“ awest”,“ swami”,“ famble”,“ mimble”,“ maxima”,“ embolo”,“ embole”,“ wamble”,“ semese”,“ semble”,“ sawbwa”,“ sawbwa” ]锯木']锯木']

注意:此程序不会输出1个字母的单词,也不会完全按单词长度过滤。这很容易添加,但与问题无关。如果可以用多种方式拼写,也会多次输出某些单词。如果给定的单词可以用许多不同的方式拼写(最坏的情况:网格中的每个字母都是相同的(例如,“ A”),而词典中的单词像“ aaaaaaaaaa”),那么运行时间将变得指数级。在算法完成之后,过滤掉重复项和进行排序是微不足道的。


14
哦 我很高兴有人上台。尽管此方法有效,但它不会“记住”它已经使用过的字母,并且会提出需要两次使用同一字母的单词,这是不允许的。当我是个白痴时,我该如何解决呢?
Paolo Bergantino,

3
是的,它不记得已访问过哪些字母,但是您的spec =)中未指定。要解决此问题,您必须向所有队列数据中添加所有已访问位置的列表,然后在添加下一个字符之前检查该列表。
亚当·罗森菲尔德

不,在BoggleWords()内部。您无需存储四元组(x,y,s,n,l),而不是存储四元组(x,y,s,n),其中l是到目前为止已访问的(x,y)的列表。然后,您针对l检查每个(x2,y2)并仅当它不在l中时才接受。然后将其添加到新的l中。
亚当·罗森菲尔德

2
当我厌倦了打乱游戏时,我也这样做了。我认为递归(DFS而不是BFS)解决方案更吸引人,因为您可以保留一组活动单元(因此您不会两次访问同一单元)。然后整整齐齐地保留一堆列表。
贾斯汀·史纳

2
这不应该陷​​入无限循环吗?我的意思是说(x,y),可能的追随者是(x+1,y+1)。然后(x+1,y+1)被推入队列。但是,(x,y)也会成为的追随者(x+1,y+1),所以这会导致他们之间不断反弹吗?
SexyBeast 2012年

39

为了加快字典速度,您可以执行一个常规转换/过程,以大大减少字典之间的比较。

鉴于上面的网格仅包含16个字符,其中一些重复,因此您可以通过简单地过滤掉具有无法达到的字符的条目来大大减少字典中键的总数。

我以为这是显而易见的优化,但是我没有提到有人提到它。

仅在输入过程中,我就从200,000个键的字典减少到了2,000个键。这至少可以减少内存开销,而且由于内存的速度不是无限快,因此这肯定会映射到某个地方的速度提高。

Perl实施

我的实现有点繁琐,因为我非常重视能够知道每个提取的字符串的确切路径,而不仅仅是其中的有效性。

我在那里也做了一些调整,理论上将允许其中有孔的网格起作用,以及具有不同尺寸线的网格(假设您正确输入并以某种方式排列)。

正如先前所怀疑的,早期过滤器是迄今为止我的应用程序中最重要的瓶颈,并指出该行将其从1.5s膨胀到7.5s。

在执行后,似乎认为所有单个数字都在其自己的有效单词上,但是由于字典文件的工作原理,我很确定这是肯定的。

它有点肿,但至少我重用了cpan的Tree :: Trie

其中有些是部分受现有实现的启发,有些是我已经想到的。

建设性的批评及其改进方法受到欢迎(/ me指出,他从未在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 ) ;
};

Arch /执行信息以进行比较:

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');
      }
  });
}
           s / iter未过滤已过滤
未过滤8.16--94%
过滤0.464 1658%-

ps:8.16 * 200 = 27分钟。


2
我知道我无法通过优化俱乐部,但是在进行代码实际工作之前我遇到了速度问题,将输入时间从2s减少到1.2s对我来说意义重大。
肯特·弗雷德里克

/ me指出,现在与将键添加到哈希中相比,正则表达式和跳过条目花费的时间更少,这很奇怪。
肯特·弗雷德里克

很好,一个Perl实现!我现在去运行它。
Paolo Bergantino,

Blerg,很难在我的Web服务器上安装Tree :: Trie。:(
Paolo Bergantino,

3
您是如何生成最后的报告(架构/执行信息)的?看起来很有用。
jmanning2k

33

您可以将问题分为两部分:

  1. 某种搜索算法将枚举网格中可能的字符串。
  2. 测试字符串是否为有效单词的一种方法。

理想情况下,(2)还应包括一种测试字符串是否为有效单词的前缀的方法-这将使您能够修剪搜索并节省大量时间。

亚当·罗森菲尔德的《特里》(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)到相应的字符。这使我可以将职位列表变成一个单词:

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

最后,我们进行深度优先搜索。基本过程是:

  1. 搜索到达特定的节点。
  2. 检查到目前为止的路径是否可以是单词的一部分。如果没有,请不要再探索此分支。
  3. 检查到目前为止的路径是否是单词。如果是这样,请添加到结果列表中。
  4. 探索到目前为止尚未走过的所有孩子。

蟒蛇:

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

该代码(从字面上看)花费了几秒钟的时间来加载字典,但是其余的在我的机器上是即时的。


非常好!也非常快。我将等一下,看看是否还有其他人站起来,但是到目前为止,您的回答看起来还不错。
Paolo Bergantino,

我很困惑为什么“ embole”是您唯一的6个字母的单词,为此我有10个不同的单词。看来您禁止两次访问同一节点,并且正如OP所述,这是公平的游戏。
肯特·弗雷德里克

1
好的,他仍然可能会遇到错误,因为他丢弃了不共享字符的“ FAMBLE”,“ WAMBLE”和“ SEMBLE”。
肯特·弗雷德里克

好眼力!错误在于创建前缀集:我需要使用range(len(w)+1)而不是range(len(w))。我声称,words <= prefixes但显然我没有测试:-/
John Fouhy

1
这帮助我学习了DFS的工作原理以及实现方法。除了发表评论外,没有其他方法可以表示对此表示赞赏。谢谢!
格雷厄姆·史密斯

23

我在Java中的尝试。读取文件和构建树需要大约2 s,而解决难题大约需要50 ms。我使用了问题中链接的字典(其中有一些我不知道的英文单词,例如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;
    }
}

1
我正在将我的输出与其他StackOverflowers的输出进行比较,似乎Adam,John和rvarcher的输出缺少一些文字。例如,“ Mwa”在字典中(是!),但不会在Adam,John和rvarcher的输出中返回。它在Paolo的PHP链接中返回两次。
gineer

1
我通过复制粘贴尝试了此操作。它显示“正在阅读...”和“完成阅读...”,但此后没有任何显示。没有显示匹配项。
MikkoP 2012年

23

我认为您可能会花费大部分时间尝试匹配字母网格无法构建的单词。因此,我要做的第一件事就是尝试加快这一步,这应该会带给您大部分帮助。

为此,我将表格重新表达为可能的“动作”表,您可以通过查找的字母转换对其进行索引。

首先为每个字母分配整个字母的数字(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

然后,创建一个二维布尔数组,该数组告诉您是否有某个可用的字母转换:

     |  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

如果全部允许,则有可能找到该词。

例如,可以在第四个转换(m到e:helMEt)中排除单词“ helmet”,因为表中的该条目为false。

并且可以排除仓鼠一词,因为不允许进行第一个(h到a)转换(甚至在您的表中也不存在)。

现在,对于您可能还没有消除的剩余单词,尝试以现在的方式或此处其他一些答案中的建议,在网格中实际找到它们。这是为了避免由于网格中相同字母之间的跳转而导致误报。例如,表允许使用“帮助”一词,但网格不允许。

关于此想法的一些进一步的性能改进技巧:

  1. 代替使用2D数组,而是使用1D数组并自己简单地计算第二个字母的索引。因此,代替上面的12x12数组,创建一个长度为144的1D数组。如果您始终使用相同的字母(即标准英语字母为26x26 = 676x1数组),即使不是所有字母都显示在网格中,您可以将索引预先计算到此一维数组中,以进行测试以匹配字典单词。例如,上例中“ 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个字母的组合。这样,您可以立即消除更多的单词,并且将每个单词的数组查找数量减少1:对于“ hello”,您只需要3个数组查找:hel,ell,llo。顺便说一句,构建该表将很快,因为网格中只有400个可能的3个字母的移动。

  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中预先计算表格。在该数组中包含索引。

我敢肯定,如果您使用了这种方法,并且您已经预先计算了词典并且已经将其加载到内存中,则可以使您的代码快速运行。

顺便说一句:如果您要开发游戏,另一件事是在后台立即运行此类操作。当用户仍在查看应用程序的标题屏幕并使手指放到按下“播放”的位置时,开始生成和解决第一个游戏。然后在用户玩前一个游戏时生成并解决下一个游戏。那应该给您很多时间来运行代码。

(我喜欢这个问题,所以我很可能会在未来的某个日子用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

对于以下5x5网格:

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

它给出了这个:

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)引起的,我不知道。但这比原始问题末尾引用的其他实现的结果快得多。因此,此算法是否优于trie词典,目前我还不知道。

  • 所使用的表格方法checkWordTriplets()非常接近实际答案。只有1中只有3-5个单词通过了将失败的checkWords()测试(参见候选人数实际数字以上)。

  • 您在上面看不到的东西:该checkWordTriplets()功能大约需要3.65毫秒,因此在搜索过程中完全占主导地位。该checkWords()功能几乎占用了剩余的0.05-0.20毫秒。

  • checkWordTriplets()函数的执行时间与字典大小线性相关,并且实际上与电路板大小无关!

  • 的执行时间checkWords()取决于电路板的大小和不能排除的字数checkWordTriplets()

  • checkWords()上面的实现是我想到的最笨的第一个版本。基本上根本没有优化。但是checkWordTriplets()与之相比,它与应用程序的总体性能无关紧要,因此我不必担心。但是,如果电路板尺寸变大,此功能将变得越来越慢,并最终开始变得重要。然后,还需要对其进行优化。

  • 这段代码的优点是它的灵活性:

    • 您可以轻松更改电路板尺寸:更新第10行,并将String数组传递给initializeBoard()
    • 它可以支持更大/不同的字母,并且可以处理诸如将“ Qu”视为一个字母之类的事情,而没有任何性能开销。为此,您需要更新第9行以及将字符转换为数字的两个位置(当前只需从ASCII值中减去65)

好的,但是我认为到目前为止,这篇文章已经足够长了。我绝对可以回答您可能遇到的任何问题,但让我们将其移至评论中。


好答案。我想看看您在Java中的实现。
MikkoP 2012年

@MikkoP完成!:)花费了大约3个小时和220行代码。度过一个下午的好方法。让我知道您是否对其工作方式有任何疑问... :)
Markus A.

感谢您发布代码!添加缺失的导入内容后,我用自己的字典尝试了此操作。我在行上得到一个ArrayIndexOutOfBoundException ok = possibleTriplets[entry.triplets[t]];。嗯?
MikkoP 2012年

@MikkoP当前编写此代码是为了假定字典仅包含大写字母AZ。关键在第34行:entry.letters[i] = (byte) letter - 65;它仅采用ASCII值并减去65(“ A”)。如果您的词典中有Umlauts或小写字母,这将提供大于31的值,这不是第9行中设置的字母大小所计划的。要支持其他字母,您必须扩展此行将它们映射到字母大小允许的范围内。
Markus A.

1
@AlexanderN您可能正确理解了逻辑。我在复制字母网格时犯了一个错误...对不起...(已修复)
Markus A.

19

令人惊讶的是,没有人尝试过此版本的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秒钟,但在我的Web服务器上却需要5秒钟。无论哪种情况,它都不是很快。不过,这还是很可怕的,因此我可以想象时间可以大大减少。任何有关如何实现这一目标的指针将不胜感激。PHP缺少元组,使坐标处理变得很奇怪,而我无法理解到底发生了什么却根本没有帮助。

编辑:一些修复使它在本地花费不到1s。


+1 @“我无法理解到底发生了什么事,这完全没有帮助。” 大声笑。我爱诚实!
dna123

我不知道PHP,但是我要尝试的第一件事是提升'/ ['。implode('',$ alphabet)。'] {3,} $ /'循环外。也就是说,为此设置一个变量,然后在循环内使用该变量。
Darius Bacon

我很确定PHP会保留编译后的正则表达式的每个线程的全局缓存,但是无论如何我都会尝试的。
Paolo Bergantino,

1
@Daniel:显然这是我的Web服务器。在本地运行时不会发生。耸耸肩。真的不喜欢把它弄下来。
Paolo Bergantino,2009年

2
最后,在find_words函数中应将什么设置为7.参数?
MikkoP 2012年

16

对VB不感兴趣?:)我无法抗拒。我解决的方法与此处介绍的许多解决方案不同。

我的时间是:

  • 将字典和单词前缀加载到哈希表中:0.5至1秒。
  • 查找单词:平均不到10毫秒。

编辑:Web主机服务器上的词典加载时间比我的家用计算机长约1到1.5秒。

我不知道随着服务器负载的增加,时间会变得越来越糟。

我将解决方案写为.Net中的网页。myvrad.com/boggle

我正在使用原始问题中引用的字典。

单词不能重复使用。仅找到3个字符或更长的单词。

我正在使用所有唯一单词前缀和单词的哈希表,而不是trie。我不了解特里,所以我在那里学到了一些东西。除了完整的单词之外,创建单词前缀列表的想法最终使我的工作降到了可观的数量。

阅读代码注释以获取更多详细信息。

这是代码:

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

我将假设您在这里使用了ap系统而不是[x] [y],因为后者在VB中相当复杂?我花了一天的时间试图获得一次2向动态数组,即:array(array(1,(hello)),1,“ hello”,array()),仍然不知道该怎么做那:P
肯特·弗雷德里克

在PHP和Perl 2中,暗淡的数组很有趣。可以在VB中完成,但我不会称其为有趣的过程。Dim Arr(,)As Integer = {{1,1},{0,0}}。AP流程是出于我的想法,问我:“从这里到哪里去?” 我知道这是一个严格的解决方案,但它在这里有效。
rvarcher

哦,我喜欢VB.NET ...我尝试了URL,但是没有用。我不得不将自己的代码重新构建为Windows窗体,并且它可以正常工作。谢谢。
艾哈迈德·埃萨斯

11

一看到问题陈述,我就想到“特里”。但是看到其他几位发帖者都使用了这种方法,我就寻找了另一种与众不同的方法。,, Trie方法的效果更好。我在自己的机器上运行了Kent的Perl解决方案,并使其适应了我的字典文件后,运行了0.31秒。我自己的perl实现需要0.54秒才能运行。

这是我的方法:

  1. 创建一个过渡哈希来模拟合法的过渡。

  2. 遍历所有16 ^ 3个可能的三个字母组合。

    • 在循环中,请排除非法转场并重复访问同一广场。形成所有合法的三字母序列并将其存储在哈希中。
  3. 然后遍历字典中的所有单词。

    • 排除太长或太短的单词
    • 在每个单词上滑动一个3字母的窗口,查看它是否属于步骤2的3字母组合。排除失败的单词。这消除了大多数不匹配项。
    • 如果仍不能消除,则使用递归算法来查看单词是否可以通过穿过拼图来构成。(这部分很慢,但是很少调用。)
  4. 打印出我发现的单词。

    我尝试了3个字母和4个字母的序列,但是4个字母的序列使程序变慢了。

在我的代码中,我将/ usr / share / dict / words用于字典。它是MAC OS X和许多Unix系统上的标准配置。您可以根据需要使用其他文件。要破解另一个难题,只需更改变量@puzzle。这将很容易适应较大的矩阵。您只需要更改%transitions哈希和%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.
}

词典的位置是否已更改?我想找到字典中的单词,因为我想与所有人比较我的解决方案,但在/ usr / share / dict的给定链接上找不到它。我知道这是一个很老的话题,但是如果您能指出我的话,那就太好了。在此先感谢您的帮助。
Naman 2015年

目前暂时没有Mac。您只需要一个英文单词的文件,一行到一行,以换行符分隔。您可以在互联网上找到这样的文件。一个是在这里:mieliestronk.com/corncob_lowercase.txt, 但是可能有更多单词的列表。
Paul Chernoch 2015年

非常感谢您的回复。我已经在ubuntu文件中找到了。
Naman 2015年

9

我知道我来晚了,但是前一段时间我用PHP做到了其中之一-只是为了好玩...

http://www.lostsockdesign.com.au/sandbox/boggle/index.php?letters=fxieamloewbxastu0.90108秒内找到75个单词(133分)

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

给出程序实际上在做什么的指示-每个字母都是在每个'。'处开始浏览模式的地方。显示了它尝试采用的路径。越“。” 有进一步的搜索。

让我知道您是否想要代码...这是PHP和HTML的可怕结合,从来没有想过今天,所以我不敢在这里张贴它:P


9

我花了3个月的时间来解决10个最佳点密集5x5 Boggle电路板问题。

现在已解决问题,并在5个网页上进行了全面披露。如有疑问,请与我联系。

棋盘分析算法使用显式堆栈通过具有直接子信息的有向非循环字图和时间戳跟踪机制伪递归遍历棋盘方块。这很可能是世界上最先进的词典数据结构。

该方案每秒在四核上评估大约10,000个非常好的板。(9500+分)

父网页:

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

组件网页:

最佳记分板-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

-这项全面的工作只会吸引最有需求的人。


4
您的分析很有趣,但从技术上来讲,您的结果并不是Boggle主板。5x5的Boggle游戏包含一个包含面孔BJKQXZ的骰子,您的实现方式明确排除了所有这些字母,因此在真正的Boggle游戏中实际上不可能实现棋盘位置。
MarkPflug 2012年

4

搜索算法是否会随着搜索的继续不断减少单词列表?

例如,在上面的搜索中,您的单词只能以13个字母开头(有效地减少为起始字母的一半)。

当您添加更多字母排列时,它将进一步减少可用的单词集,从而减少必要的搜索。

我从那里开始。


4

我将不得不对一个完整的解决方案进行更多的思考,但是为了方便起见,我想知道是否有必要根据所有字典中的单词,并以此优先搜索。我会用单词的开头字母。因此,如果您的词典包含单词“ India”,“ Water”,“ Extreme”和“ Extraordinary”,则您预先计算的表可能是:

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

然后按通用性顺序搜索这些图(首先是EX,然后是WA / IN)


4

首先,了解一位C#语言设计师如何解决相关问题:http : //blogs.msdn.com/ericlippert/archive/2009/02/04/a-nasality-talisman-for-the-sultana-analyst.aspx

像他一样,您可以从字典开始,然后通过从一系列按字母顺序排列的字母到可以用这些字母拼写的单词列表创建字典来规范单词。

接下来,开始从板上创建可能的单词并查找它们。我怀疑这会让您走得很远,但是肯定还有更多的技巧可以加快速度。


4

我建议根据单词制作一棵字母树。该树将由字母结构组成,如下所示:

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.

可以通过一些动态编程来加快速度。例如,在您的示例中,两个“ A”都紧挨着“ E”和“ W”,这两个点(从它们被击中的角度来看)是相同的。我没有足够的时间来真正地为此编写代码,但是我认为您可以理解这个想法。

另外,如果您使用Google的“ Boggle求解器”,我相信您还会找到其他解决方案。



3

搞笑 几天前,由于该死的游戏,我几乎发布了相同的问题!但是我没有,因为只是在Google上搜索了Boggle Solver Python,并得到了我想要的所有答案。


我不知道它的通俗名称是“ boggle”,但是我确实在google上找到了一些东西,我只是很好奇地看到人们会对SO提出什么建议。:)
Paolo Bergantino,

3

我知道这个问题的时机已经过去,但由于我自己是一个求解器的工作者,并且在进行谷歌搜索时偶然发现了这个问题,因此我认为我应该发布我的参考文献,因为这似乎与其他参考文献有所不同。

我选择在游戏板上使用平面阵列,并从游戏板上的每个字母进行递归搜索,从有效邻居遍历到有效邻居,如果当前字母列表(如果索引中包含有效前缀)则扩展搜索。在遍历当前单词的概念时,要注意的是板索引列表,而不是组成单词的字母。检查索引时,索引将转换为字母并完成检查。

该索引是一个蛮力字典,有点像特里,但允许对索引进行Python查询。如果列表中包含单词“ 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。

如果感觉像这样允许一些看起来不太慢的可读代码。像其他所有人一样,该系统的开销是在读取/建立索引。解决电路板问题很大。

该代码位于http://gist.github.com/268079。它故意垂直且幼稚,带有大量显式的有效性检查,因为我想了解问题,而不用一堆魔术或晦涩难解。


3

我用C ++编写了求解器。我实现了自定义树结构。我不确定它是否可以被认为是Trie,但它是相似的。每个节点有26个分支,每个字母1个。我与字典的各个分支平行地遍历防虫板的各个分支。如果字典中不存在该分支,则停止在Boggle板上搜索它。我将黑板上的所有字母都转换为整数。因此'A'=0。由于它只是数组,因此查找始终为O(1)。每个节点都存储是否完成一个单词以及其子代中存在多少个单词。当找到单词以减少重复搜索相同单词时,会修剪树。我相信修剪也是O(1)。

处理器:奔腾SU2700 1.3GHz
RAM:3GB

在<1秒内加载178,590个单词的字典。
在4秒内解决100x100 Boggle(boggle.txt)。找到约44,000个单词。
解决4x4 Boggle问题太快了,无法提供有意义的基准测试。:)

Fast Boggle Solver GitHub存储库


2

给定一个具有N行M列的Boggle开发板,我们假设以下内容:

  • N * M实质上大于可能的字数
  • N * M实质上大于最长的单词

在这些假设下,该解决方案的复杂度为O(N * M)。

我认为以多种方式比较该示例板的运行时间没有抓住重点,但是出于完整性考虑,此解决方案在现代MacBook Pro上的完成时间不到0.2秒。

该解决方案将为语料库中的每个单词找到所有可能的路径。

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

该解决方案还提供了在给定板中搜索的方向

算法:

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

1

我已经在OCaml中实现了一个解决方案。它以字典树的形式预编译字典,并使用两个字母的序列频率来消除单词中永远不会出现的边缘,从而进一步加快处理速度。

它可以在0.35毫秒内解决您的示例电路板(额外的6毫秒启动时间,这主要与将Trie加载到内存有关)。

找到的解决方案:

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

很好,但是这里发布的所有时间都涉及将字典加载到内存中的任何“启动”时间,因此将0.35与其他时间进行比较非常不准确。另外,您是否使用其他词典?你错过了一些话。无论哪种方式,+ 1
Paolo Bergantino 2012年

启动时间需要6毫秒,因此您需要等待6.35毫秒才能完整运行。我正在使用本地/usr/share/dict词典,确实缺少某些单词(例如EMBOLE)。
Victor Nicollet

1

Node.JS JavaScript解决方案。在不到一秒钟的时间内计算所有100个唯一单词,包括读取字典文件(MBA 2012)。

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

码:

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

因此,我想添加另一种解决该问题的PHP方法,因为每个人都喜欢PHP。我想做一些重构,例如对字典文件使用正则表达式匹配,但是现在我只是将整个字典文件加载到wordList中。

我使用链接列表的想法来做到这一点。每个节点都有一个字符值,一个位置值和一个下一个指针。

位置值是我如何确定是否连接了两个节点。

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

因此,使用该网格,如果第一个节点的位置等于第二行的+/- 1(同一行,上下一行的+/- 9、10、11),我知道两个节点已连接。

我使用递归进行主要搜索。它从wordList中删除一个单词,找到所有可能的起点,然后递归地找到下一个可能的连接,请记住,它不能转到已经使用的位置(这就是为什么我添加$ notInLoc的原因)。

无论如何,我知道它需要一些重构,并且很想听听有关如何使其更整洁的想法,但是它会根据我使用的字典文件产生正确的结果。根据板上元音和组合的数量,大约需要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

我知道我参加聚会真的很晚,但是作为编码练习,我已经用几种编程语言(C ++,Java,Go,C#,Python,Ruby,JavaScript,Julia,Lua,PHP,Perl)实现了一个令人讨厌的求解器,并且我以为有人可能对此感兴趣,所以我在这里留下链接:https : //github.com/AmokHuginnsson/boggle-solvers


1

这是在NLTK工具箱中使用预定义单词的解决方案NLTK具有nltk.corpus软件包,其中我们有名为word的软件包,其中包含2个以上的Lakhs英语单词,您可以在程序中简单地使用所有单词。

创建矩阵后,将其转换为字符数组并执行此代码

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

希望您能理解。


0

这是我的Java实现:https : //github.com/zouzhile/interview/blob/master/src/com/interview/algorithms/tree/BoggleSolver.java

Trie构建花费了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
型号标识符:MacBookPro8,1
处理器名称:Intel Core i5
处理器速度:2.3 GHz
处理器数量:1
内核总数:2
L2缓存(每个核心):256 KB
L3缓存:3 MB
内存:4 GB
引导ROM版本:MBP81.0047.B0E
SMC版本(系统):1.68f96


0

我也用Java解决了这个问题。我的实现是269行,很容易使用。首先,您需要创建Boggler类的新实例,然后使用网格作为参数来调用solve函数。将大约50000个单词的词典加载到我的计算机上大约需要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

我在c中解决了这个问题。在我的计算机上运行大约需要48毫秒(大约98%的时间用于从磁盘加载字典并创建Trie)。字典是/ usr / share / dict / american-english,有62886个单词。

源代码


0

我完美而快速地解决了这个问题。我把它放到了一个Android应用程序中。在Play商店链接中观看视频,观看视频。

Word Cheats是一款可以“破解”任何矩阵风格的文字游戏的应用程序。该应用程序旨在帮助我在单词打扰器中作弊。它可以用于单词搜索,鼻塞,单词,单词查找器,单词破解,过渡等等!

可以在这里 https://play.google.com/store/apps/details?id=com.harris.wordcracker

通过视频https://www.youtube.com/watch?v=DL2974WmNAI查看运行中的应用

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.