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

How to find list of possible words from a letter matrix [Boggle Solver]

最近我在我的iPhone上玩了一个叫做"混乱"的游戏。你们中的一些人可能知道这个游戏是令人难以置信的。基本上,当游戏开始时,你会得到一个类似这样的字母矩阵:

1
2
3
4
F X I E
A M L O
E W B X
A S T U

游戏的目标是找到尽可能多的单词,这些单词可以通过将字母链接在一起形成。你可以从任何一个字母开始,围绕它的所有字母都是公平的游戏,然后一旦你进入下一个字母,围绕着这个字母的所有字母都是公平的游戏,除了以前使用过的任何字母。例如,在上面的网格中,我可以想出单词LOBTUXSEAFAME等。单词必须至少有3个字符,并且不超过nxn字符,在这个游戏中为16个,但在某些实现中可能有所不同。虽然这个游戏很有趣而且让人上瘾,但我显然不太擅长,我想通过制作一个能给我最好的单词(单词越长,得分越高)的程序来作弊。

Sample Boggle(source:boggled.org)

不幸的是,我不太擅长算法或它们的效率等等。我的第一次尝试使用一个字典,比如这个(~2.3MB),并进行线性搜索,试图匹配字典条目的组合。这需要很长的时间才能找到可能的单词,而且由于每轮只有2分钟,所以这是不足够的。

我有兴趣看看是否有任何堆垛机可以想出更有效的解决方案。我主要是使用大3 PS:Python、PHP和Perl来寻找解决方案,尽管Java或C++的任何东西都很酷,因为速度是必不可少的。

当前解决方案:

  • 亚当·罗森菲尔德,Python,~20岁
  • 约翰·福伊,Python,~3岁
  • 肯特·弗雷德里克,波尔,~1
  • 大流士培根,Python,~1
  • rvarcher,vb.net(实时链接),~1s
  • paolo bergantino,php(实时链接),~5s(本地~2s)

BOUNTY:

我在这个问题上加了一个赏金,作为我对所有参与他们项目的人的感谢。不幸的是,我只能给你们中的一个人一个公认的答案,所以我将在7天后测量谁拥有最快的解算器,并奖励获奖者奖金。

奖励奖金。感谢所有参与的人。


我的答案和这里的其他答案一样,但是我会发布它,因为它看起来比其他的Python解决方案要快一些,从更快地建立字典开始。(我对照约翰·福伊的解决方案检查了这个问题。)设置之后,解决问题的时间就在噪音中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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('
') 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)

样品使用情况:

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

编辑:筛选出长度小于3个字母的单词。

编辑2:我很好奇为什么Kent Fredric的Perl解决方案更快;结果是使用正则表达式匹配而不是一组字符。在python中做同样的操作,速度会提高一倍。


你将要得到的最快的解决方案可能包括把你的字典存储在一个trie中。然后,创建一个由三元组(x,y,s)组成的队列,其中队列中的每个元素对应一个单词的前缀s,该单词可以在网格中拼写,以位置(x,y)结尾。用n x n个元素初始化队列(其中n是网格的大小),网格中每个正方形对应一个元素。然后,算法进行如下操作:

1
2
3
4
5
While the queue is not empty:
  Dequeue a triple (x, y, s)
  For each square (x', y') with letter c adjacent to (x, y):
    If s+c is a word, output s+c
    If s+c is a prefix of a word, insert (x', y', s+c) into the queue

如果将字典存储在trie中,则可以在恒定时间内测试s+c是单词还是单词的前缀(前提是在每个队列数据中还保留一些额外的元数据,例如指向trie中当前节点的指针),因此该算法的运行时间为o(可以拼写的单词数)。

[编辑]下面是我刚刚编码的python中的一个实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#!/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

示例用法:

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

输出:

['fa', 'xi', 'ie', 'io', 'el', 'am', 'ax', 'ae', 'aw', 'mi', 'ma', 'me', 'lo', 'li', 'oe', 'ox', 'em', 'ea', 'ea', 'es', 'wa', 'we', 'wa', 'bo', 'bu', 'as', 'aw', 'ae', 'st', 'se', 'sa', 'tu', 'ut', 'fam', 'fae', 'imi', 'eli', 'elm', 'elb', 'ami', 'ama', 'ame', 'aes', 'awl', 'awa', 'awe', 'awa', 'mix', 'mim', 'mil', 'mam', 'max', 'mae', 'maw', 'mew', 'mem', 'mes', 'lob', 'lox', 'lei', 'leo', 'lie', 'lim', 'oil', 'olm', 'ewe', 'eme', 'wax', 'waf', 'wae', 'waw', 'wem', 'wea', 'wea', 'was', 'waw', 'wae', 'bob', 'blo', 'bub', 'but', 'ast', 'ase', 'asa', 'awl', 'awa', 'awe', 'awa', 'aes', 'swa', 'swa', 'sew', 'sea', 'sea', 'saw', 'tux', 'tub', 'tut', 'twa', 'twa', 'tst', 'utu', 'fama', 'fame', 'ixil', 'imam', 'amli', 'amil', 'ambo', 'axil', 'axle', 'mimi', 'mima', 'mime', 'milo', 'mile', 'mewl', 'mese', 'mesa', 'lolo', 'lobo', 'lima', 'lime', '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', 'awest', 'mamie', 'mambo', 'maxim', 'mease', 'mesem', 'limax', 'limes', 'limbo', 'limbu', 'obole', 'emesa', 'embox', 'awest', 'swami', 'famble', 'mimble', 'maxima', 'embolo', 'embole', 'wamble', 'semese', 'semble', 'sawbwa', 'sawbwa']

注意:这个程序不输出1个字母的单词,也不按单词长度过滤。这很容易添加,但与问题并不真正相关。如果一些单词可以用多种方式拼写,它也会多次输出这些单词。如果一个给定的单词可以用许多不同的方式拼写(最坏的情况是:网格中的每个字母都是相同的(例如"a"),并且字典中有一个类似"aaaaaaaaa"的单词),那么运行时间将得到可怕的指数级增长。在算法完成后,过滤掉重复的数据并进行排序是很简单的。


为了加快字典的速度,您可以做一个常规的转换/过程来大大减少提前进行的字典比较。

考虑到上面的网格只包含16个字符,其中一些是重复的,您可以通过简单地筛选出具有不可访问字符的条目来大大减少字典中的总关键字数。

我认为这是一个明显的优化,但看到没人做,我就提到了。

它将我从一本200000个键的字典简化为仅在输入过程中使用2000个键。这至少可以减少内存开销,而且由于内存不是无限快的,所以这肯定会映射到某个地方的速度增加。

Perl实现

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

我在这里也有一些修改,理论上允许一个有孔的网格运行,和有不同大小的线的网格(假设你得到了正确的输入,并且它以某种方式排列)。

正如前面怀疑的那样,早期的过滤器是到目前为止我的应用程序中最重要的瓶颈,它将行从1.5扩展到7.5。

在执行时,它似乎认为所有的单个数字都在它们自己的有效字上,但我很确定这是由于字典文件的工作方式。

它有点膨胀,但至少我重用了CPAN中的tree::trie

其中一些灵感部分来自于现有的实现,其中一些我已经想到了。

建设性的批评和提高欢迎程度的方法(我注意到他从未在CPAN中搜索过令人难以置信的解决方案,但这更有趣)

为新标准更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
#!/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",
", map { $_->pp } @{
  traverseGraph( $d, $m, 3, $c ) ;
};

用于比较的架构/执行信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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% ==============================

更多关于regex优化的抱怨

我使用的regex优化对于多解字典是无用的,对于多解字典,您需要一个完整的字典,而不是一个预修剪的字典。

但是,也就是说,对于一次性解决方案,它的速度非常快。(perl regex在C中!:)

下面是一些不同的代码添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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');
      }
  });
}
1
2
3
           s/iter unfiltered   filtered
unfiltered   8.16         --       -94%
filtered    0.464      1658%         --

ps:8.16*200=27分钟。


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

  • 一种在网格中枚举可能字符串的搜索算法。
  • 一种测试字符串是否为有效字的方法。
  • 理想情况下,(2)还应该包括一种测试字符串是否是有效单词前缀的方法——这将允许您修剪搜索并节省整个时间堆。

    亚当·罗森菲尔德的《特里尔》是对(2)的一个解决方案。它很优雅,也许是您的算法专家喜欢的,但是有了现代语言和现代计算机,我们可能会更加懒惰。另外,正如肯特所建议的,我们可以通过丢弃网格中没有字母的单词来减小字典的大小。下面是一些Python:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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)部分,我倾向于用图表来思考。所以我要建立一个像这样的字典:

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

    也就是说,graph[(x, y)]是从位置(x, y)可以到达的一组坐标。我还将添加一个虚拟节点None,它将连接到所有内容。

    建立它有点笨拙,因为有8个可能的位置,你必须做边界检查。下面是一些相应的笨拙的python代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    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)映射到相应的字符。这让我把一个职位列表变成一个词:

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

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

  • 搜索到达特定节点。
  • 检查到目前为止的路径是否是单词的一部分。如果没有,就不要再深入研究这个分支。
  • 检查到目前为止的路径是否是一个单词。如果是,请添加到结果列表中。
  • 探索到目前为止还不是道路一部分的所有儿童。
  • Python:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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)

    代码运行方式:

    1
    2
    3
    4
    5
    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查看答案。以下是为您的示例找到的单词列表,按大小排序:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
     ['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']

    代码需要(字面上)几秒钟来加载字典,但其余的代码在我的机器上是即时的。


    我想你可能会花大部分时间去尝试匹配那些不可能由你的字母网格构建的单词。所以,我要做的第一件事就是试着加快这一步,这会让你走得更远。好的。

    为此,我将把网格重新表示为一个可能的"移动"表,您可以通过正在查看的字母转换对其进行索引。好的。

    首先从整个字母表中给每个字母分配一个数字(A=0,B=1,C=2,……等等。好的。

    举个例子:好的。

    1
    2
    3
    4
    h b c d
    e e g h
    l l k l
    m o f p

    现在,让我们用我们现有的字母表(通常你可能每次都想用相同的整个字母表):好的。

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

    然后生成一个二维布尔数组,告诉您是否有可用的字母转换:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
         |  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

    现在浏览单词列表并将单词转换为转换:好的。

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

    然后通过在表中查找这些转换来检查是否允许:好的。

    1
    2
    3
    4
    [6][ 3] : T
    [3][ 8] : T
    [8][ 8] : T
    [8][10] : T

    如果他们都被允许,就有可能找到这个词。好的。

    例如,"头盔"一词可以在第四个转换(M到E:头盔)中排除,因为您表中的条目是假的。好的。

    可以排除"仓鼠"这个词,因为第一个(h到a)转换是不允许的(甚至不存在于您的表中)。好的。

    现在,对于那些你没有删掉的单词,试着用你现在的方式在网格中找到它们,或者按照这里其他答案中的建议找到它们。这是为了避免由于网格中相同字母之间的跳跃而产生的误报。例如,表允许"帮助"一词,但网格不允许。好的。

    关于这个想法的一些进一步的性能改进提示:好的。

  • 不要使用二维数组,而是使用一维数组,自己简单地计算第二个字母的索引。因此,不要像上面那样使用12x12数组,而是使用长度为144的1d数组。如果您总是使用相同的字母表(即标准英文字母表的26x26=676x1数组),即使不是所有的字母都显示在您的网格中,您也可以将索引预计算到这个1d数组中,您需要测试它来匹配字典中的单词。例如,上面示例中"hello"的索引是好的。

    1
    2
    3
    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
  • 将想法扩展到一个3D表(表示为一维数组),即所有允许的3个字母组合。这样,您可以立即删除更多的单词,并将每个单词的数组查找数减少1:对于"hello",只需要3个数组查找:hel、ell、llo。顺便说一句,这张桌子很快就能建成,因为网格中只有400个可能的3个字母的移动。好的。

  • 预先计算网格中需要包含在表中的移动的索引。对于上面的示例,您需要将以下条目设置为"true":好的。

    1
    2
    3
    4
    5
    6
    7
    (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]
    .
    :
  • 还可以用一个1-D数组来表示游戏网格,其中有16个条目,并在3中预先计算表。将索引包含到此数组中。
  • 我敢肯定,如果您使用这种方法,您可以让您的代码运行得非常快,如果您有预先计算好的字典,并已经加载到内存中。好的。

    顺便说一句:如果你正在构建一个游戏,另一个很好的方法就是在后台立即运行这些东西。当用户仍在查看应用程序的标题屏幕时,开始生成和解决第一个游戏,并将手指放在适当位置,按"播放"。然后生成并解决下一个游戏,就像用户玩前一个游戏一样。这会给你很多时间来运行你的代码。好的。

    (我喜欢这个问题,所以我很有可能会在未来几天在Java中实现我的建议,看看它实际上是如何执行的……我会在这里张贴代码。)好的。

    更新:好的。

    好的,今天我花了一些时间在Java中实现了这个想法:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    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…)中发布的图片中的网格:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    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…)好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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网格:好的。

    1
    2
    3
    4
    5
    R P R I T
    A H H L N
    I E T E P
    Z R Y S G
    O G W E Y

    它给出了:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    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 Nicolet的OCAML实现性能慢10倍。这是否是由不同的算法引起的,他使用的字典越短,他的代码被编译,我的代码在Java虚拟机中运行,或者我们的计算机的性能(我的是英特尔Q6600 @ 2.4MHz运行Win XP),我不知道。但它比原始问题末尾引用的其他实现的结果快得多。所以,不管这个算法是否优于trie字典,我现在还不知道。好的。

    • checkWordTriplets()中使用的表法得到了与实际答案非常好的近似值。只有3-5个单词中的1个通过了checkWords()测试(参见候选词数量与上述实际单词数量)。好的。

    • 上面你看不到:checkWordTriplets()函数大约需要3.65ms,因此在搜索过程中完全占据主导地位。checkWords()功能几乎占据了剩下的0.05-0.20 ms。好的。

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

    • checkWords()的执行时间取决于主板大小和checkWordTriplets()未排除的字数。好的。

    • 上面的checkWords()实现是我想到的最愚蠢的第一个版本。它基本上没有优化。但与checkWordTriplets()相比,它与应用程序的总体性能无关,所以我不担心。但是,如果电路板尺寸变大,这个功能将变得越来越慢,并最终开始起作用。然后,它也需要进行优化。好的。

    • 这段代码的一个好处是它的灵活性:好的。

      • 您可以很容易地更改板的大小:更新第10行和传递给initializeBoard()的字符串数组。
      • 它可以支持较大/不同的字母,并且可以处理像将"qu"作为一个字母这样的事情,而不需要任何性能开销。为此,需要更新第9行以及将字符转换为数字的两个位置(目前只需从ASCII值中减去65即可)。

    好吧,但我想现在这篇文章已经够长了。我可以肯定地回答你可能有的任何问题,但让我们把它放到评论中去。好的。好啊。


    我在Java中的尝试。读取文件和构建trie大约需要2秒,解决这个难题大约需要50毫秒。我用的是与这个问题相关联的字典(它有一些我不知道的单词存在于英语中,比如fae,ima)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    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的正确做法,请告诉我)。

    Engineer.BoggleSolver.main工程师

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    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();

        }
    }

    工程.bogglesolver.solver

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    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);
        }

    }

    enginer.bogglesolver.trie.node

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    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;
        }
    }

    工程设计.bogglesolver.trie.trie

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    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());
        }


    }

    Engineer.BoggleSolver.Util.Constants

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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;
    }

    enginer.bogglesolver.util.util(引擎.bogglesolver.util)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    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;
        }
    }


    令人惊讶的是,没有人尝试过这种PHP版本。

    这是John Fouhy的Python解决方案的一个有效的PHP版本。

    虽然我从其他人的答案中得到了一些提示,但这主要是从约翰那里抄来的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    $boggle ="fxie
               amlo
               ewbx
               astu";

    $alphabet = str_split(str_replace(array("
    ","","
    "),"", strtolower($boggle)));
    $rows = array_map('trim', explode("
    ", $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缺少元组,这使得坐标的使用变得很奇怪,我无法理解到底发生了什么,这一点都没有帮助。

    编辑:一些修正使它在本地不到1秒。


    对VB不感兴趣?:)我无法抗拒。我解决这个问题的方法与这里提出的许多解决方案不同。

    我的时代是:

    • 正在将字典和单词前缀加载到哈希表中:.5到1秒。
    • 找到单词:平均不到10毫秒。

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

    我不知道随着服务器负载的增加,情况会恶化到什么程度。

    我在.NET中以网页形式编写了解决方案。myvrad.com/boggle

    我用的是原始问题中引用的词典。

    字母不能在单词中重复使用。只找到3个字符或更长的单词。

    我使用的是一个包含所有唯一单词前缀和单词的哈希表,而不是trie。我不知道特里尔的事,所以我在那里学到了一些东西。除了完整的单词之外,还要创建一个单词前缀列表的想法最终使我的时间降到了一个值得尊敬的数字。

    阅读代码注释了解更多详细信息。

    代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    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


    我一看到问题陈述,就想"trie"。但是当看到其他几张海报使用这种方法时,我寻找另一种不同的方法。唉,trie方法表现得更好。我在我的机器上运行了Kent的Perl解决方案,在修改它以使用我的字典文件之后,运行它花费了0.31秒。我自己的Perl实现需要0.54秒才能运行。
    BR/>

    这是我的方法:

  • 创建转换哈希以模拟合法转换。

  • 迭代所有16^3个可能的三个字母组合。

    • 在循环中,排除非法转换并重复访问相同的正方形。形成所有合法的3个字母序列,并将它们存储在哈希表中。
  • 然后遍历字典中的所有单词。

    • 排除过长或过短的单词
    • 在每个单词上滑动一个3个字母的窗口,查看它是否位于步骤2中的3个字母组合框中。排除失败的词。这消除了大多数不匹配。
    • 如果仍然没有消除,请使用递归算法来查看是否可以通过在拼图中创建路径来形成单词。(这部分很慢,但很少被称为。)
  • 把我找到的单词打印出来。

    我尝试了3个字母和4个字母的序列,但4个字母的序列减慢了程序的速度。

  • 在我的代码中,我的字典使用/usr/share/dict/words。它是Mac OS X和许多Unix系统的标准配置。如果需要,可以使用其他文件。要破解不同的拼图,只需更改变量@puzzle即可。这很容易适应较大的矩阵。您只需要更改%Transitions哈希和%LegalTransitions哈希。
    BR/>

    这个解决方案的优点是代码短,数据结构简单。

    下面是Perl代码(我知道它使用了太多的全局变量):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    #!/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(/
    /, uc(readFile($wordlistFile)));
    print"Words loaded from word list:" . scalar @words ."
    ";

    print"Word file load time:" . (time - $startTime) ."
    ";
    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) ."
    ";
    my $postPrefix = time;

    my @wordsFoundInPuzzle = findWordsInPuzzle(@words);

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

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

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

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

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


    我知道我迟到了,但我不久前用PHP做了一个-只是为了好玩…

    http://www.lostsockdesign.com.au/sandbox/boggle/index.php?字母=fxieamloewbxastu在0.90108秒内找到75个单词(133分)

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

    给出了程序实际正在做什么的一些指示-每个字母都是它开始查看模式的位置,而每个"."都显示了它尝试采取的路径。"."搜索得越多。

    如果你想要密码,请告诉我…这是一个混合了php和html的可怕的东西,从来没有打算看到曙光,所以我不敢在这里发布它:p


    我花了3个月的时间来研究解决10个最佳点密度5x5转向板问题。

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

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

    该方案在四核上每秒评估大约10000个非常好的电路板。(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

    -这个综合性的工作体系只会让最需要的人感兴趣。


    为了好玩,我在bash中实现了一个。它不是超快的,而是合理的。

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


    我建议用文字做一棵字母树。树将由字母结构组成,如下所示:

    1
    2
    letter: char
    isWord: boolean

    然后建立树,每个深度添加一个新字母。换句话说,在第一个层次上,会有字母表;然后从每棵树上,会有另外26个条目,等等,直到你拼出所有的单词。抓住这棵解析过的树,它可以使所有可能的答案更快地查找。

    使用这个解析树,您可以很快找到解决方案。这是伪代码:

    1
    2
    3
    4
    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"旁边,从它们碰到的位置来看,这两个"a"是相同的。我没有足够的时间来真正地拼出这个代码,但我认为你可以理解这个想法。

    另外,我相信如果你谷歌搜索"令人困惑的解决方案",你会找到其他的解决方案。


    我得多考虑一个完整的解决方案,但作为一个方便的优化,我想知道是否值得根据字典中的所有单词预先计算一个数字和三角函数(2和3个字母组合)的频率表,并用它来优先搜索。我会用单词的开头字母。因此,如果您的字典中包含"India"、"Water"、"Extreme"和"Extrevate"等词,那么您的预计算表可能是:

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

    然后按照共性的顺序搜索这些数字图(先是ex,然后是wa/in)


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

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

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

    我从那里开始。


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

    像他一样,你可以从字典和规范化的单词开始,通过创建一个字典,从按字母顺序排序的字母数组到可以从这些字母拼写的单词列表。

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


    令人捧腹的。几天前,由于同样的游戏,我几乎发布了同样的问题!但我并没有,因为我只是在谷歌上搜索了令人难以置信的解算器python,得到了我想要的所有答案。


    我意识到这个问题的时间来了又去了,但是自从我自己在解决问题的时候,在谷歌搜索的时候偶然发现了这个问题,我想我应该发布一个我的参考,因为它看起来和其他一些有点不同。

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

    索引是一个蛮力字典,有点像trie,但允许对索引进行python查询。如果列表中有"cat"和"catet"两个词,您可以在字典中找到:

    1
    2
    3
    4
    5
    6
       d = { 'c': ['cat','cater'],
         'ca': ['cat','cater'],
         'cat': ['cat','cater'],
         'cate': ['cater'],
         'cater': ['cater'],
       }

    因此,如果当前的_字是'ca',您知道它是一个有效的前缀,因为'ca' in d返回true(所以继续板遍历)。如果当前的单词是"cat",那么您就知道它是一个有效的单词,因为它是一个有效的前缀,并且'cat' in d['cat']也返回true。

    如果觉得这样可以让一些可读的代码看起来不太慢。像其他人一样,这个系统的开销是读取/构建索引。解决这个问题相当困难。

    代码位于http://gist.github.com/268079。它是有意垂直和幼稚的,有很多明确的有效性检查,因为我想理解这个问题,而不想用一堆魔术或晦涩把它弄糟。


    我用C++编写了我的解析器。我实现了一个定制的树结构。我不确定它是否可以被视为trie,但它是类似的。每个节点有26个分支,字母表中的每个字母对应一个分支。我与我的字典的分支平行地横穿摇摆板的分支。如果字典里没有这个分支,我就停止在拼字板上搜索它。我把黑板上的所有字母都转换成整数。所以"A"=0。因为它只是数组,所以查找总是O(1)。每个节点存储是否完成一个单词以及子节点中存在多少单词。当发现单词时,树会被修剪,以减少重复搜索相同单词的次数。我认为修剪也是O(1)。

    CPU:奔腾SU2700 1.3GHz内存:3GB

    在1秒内加载178590个单词的字典。在4秒内解决100x100 boggle(boggle.txt)。找到44000个单词。解决4x4问题的速度太快,无法提供有意义的基准。:)

    快速解算器Github repo


    假设一个有n行和m列的转向板,我们假设如下:

    • n*m大大大于可能的单词数
    • n*m实质上大于最长的可能单词

    在这些假设下,这个解决方案的复杂性是O(N*M)。

    我认为在很多方面比较这一个示例板的运行时间忽略了这一点,但是,为了完整性,这个解决方案在我的现代MacBook Pro上以<0.2秒完成。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    #!/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}"

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

    它在0.35毫秒内解决了您的示例板(额外的6毫秒启动时间主要与将trie加载到内存中有关)。

    找到的解决方案:

    1
    2
    3
    4
    5
    6
    7
    ["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"]


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

    输出:"FAM"、"TUX"、"TUX"、"TUX"、"FAE"、"ELI"、"ELM"、"ELB"、"TWA"、"TWA"、"SAW"、"AMI"、"SWA"、"SWA"、"AME"、"SEA"、"SEW"、"AES"、"AWL"、"AWE"、"AWE"、"AWA"、"AWA"、"AWA"、"AWA"、"AWA"、"AWA"、"FAE"、"FAE"、"ELE"、"ELI"、"ELM"、"ELM"、"ELB"、"ELB"、"TWB"、"TWA"、"TOW"、"TWA"、"TWA"、"AWL"、"AWA"、"AWA"、"AWA"、"AWA"、"AWE"、"AWA"、"AWA"、"MIX"、"MIX"、""我想你应该知道的是"埃米尔,"韦姆","伊姆","奥姆","奥姆","维斯特","维斯特","维斯特","维斯特","维斯特","维斯特","莱姆","莱姆","索特","利玛","梅萨","梅维","莱姆","名人","阿塞姆","迈尔","阿米尔","希克斯","Seam","Semi","Swam","Ambo","Amli","Awmi","Awest","Awest","Limax","Limes","Limbu","Limbo","Embox","Semble","Embole","Wamble","Famble"]

    代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    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('
    ')

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

    所以我想添加另一种PHP方法来解决这个问题,因为每个人都喜欢PHP。有一点我想做的重构,比如使用与字典文件匹配的regexpression,但是现在我只是将整个字典文件加载到单词表中。

    我用链表的方法来做这个。每个节点都有一个字符值、一个位置值和一个下一个指针。

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

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

    所以使用这个网格,我知道如果第一个节点的位置等于第二个节点的位置,那么两个节点是相连的,同一行的位置是+/-1,上下行的位置是+/-9,10,11。

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

    无论如何,我知道它需要一些重构,我很想听听关于如何使其更清晰的想法,但是它根据我使用的字典文件生成正确的结果。根据板上的元音和组合的数量,大约需要3到6秒。我知道一旦我把字典的结果匹配起来,就会大大减少。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    <?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);

        ?>

    下面是使用NLTK工具箱中的预定义单词的解决方案nltk有nltk.corpus包,其中我们有一个名为words的包,它包含2个以上的英语单词,您可以在程序中简单地使用all。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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)

    输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    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

    我希望你明白。


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

    Algo:

    1
    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

    输出:

    1
    2
    3
    4
    5
    6
    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  → → ↑ ↘ →

    代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    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 ('
    ')

        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)

    我也用Java解决了这个问题。我的实现有269行长,而且非常容易使用。首先需要创建一个新的boggler类实例,然后调用以网格为参数的solve函数。在我的计算机上加载50000个单词的字典大约需要100毫秒,它在大约10-20毫秒内找到单词。找到的单词存储在arraylist中,foundWords

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    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;
        }
    }

    我用C语言解决了这个问题。在我的机器上运行大约需要48毫秒(大约98%的时间花费在从磁盘加载字典和创建trie上)。字典是/usr/share/dict/american english,有62886个单词。

    源代码


    我很快很完美地解决了这个问题。我把它放进了一个Android应用程序。在Play Store链接中查看视频,以查看它的实际效果。

    单词作弊是一个"破解"任何矩阵式单词游戏的应用程序。这个应用程序是建立的帮助我在拼字游戏中作弊。它可以用于单词搜索,ruzzle,words,words finder,words crack,boggle,等等!

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

    在视频中查看正在运行的应用程序https://www.youtube.com/watch?V= DL247WMNaI


    我已经用C用一个DFA算法解决了这个问题。您可以在查看我的代码

    https://github.com/attilabicsko/wordshuller/

    除了在矩阵中查找单词外,我的算法还保存了单词的实际路径,因此为了设计一个单词查找游戏,您可以检查实际路径上是否有单词。


    简单的排序和使用字典中的二进制搜索怎么样?

    在0.35秒内返回整个列表,并可以进一步优化(例如删除带有未使用字母的单词等)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    from bisect import bisect_left

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

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

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

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

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

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

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

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

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


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
        package ProblemSolving;

    import java.util.HashSet;
    import java.util.Set;

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

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

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

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

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

    }

    我知道我在晚会上真的迟到了,但是我已经实现了一个编码练习,在几个编程语言(C++,Java,GO,C,Python,Ruby,JavaScript,朱丽亚,Lua,PHP,Perl)中的一个笨拙的解决方案,我认为有人可能会对这些感兴趣,所以我在这里留下链接:https://github.com/amokhuginnsson/boggle-solvers


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    import java.util.HashSet;
    import java.util.Set;

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

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

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

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

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

    }

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

        }

    }

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

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

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

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

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

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

            }

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


    这就是我为解决这个难题而提出的解决方案。我想这是最"Python式"的做事方式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    from itertools import combinations
    from itertools import izip
    from math import fabs

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

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

    def isCombinationAllowed(combination,length,doubleLength):

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

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


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

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

    我建议您不要将此部分用于所有可能的匹配,但它实际上提供了一种检查您生成的单词是否构成有效单词的可能性:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import mechanize
    def checkWord(word):
        url ="https://en.oxforddictionaries.com/search?filter=dictionary&query="+word
        br = mechanize.Browser()
        br.set_handle_robots(False)
        response = br.open(url)
        text = response.read()
        return"no exact matches"  not in text.lower()

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

    下面是我的Java实现:HTTPSE//GITHUB/COM/ZouZIL/InVIEW/BROB/MISST/SRC/COM/IVIEW/TROME/TRAE/BGGELSOLVR.java

    Trie生成花费了0小时、0分钟、1秒、532毫秒单词搜索耗时0小时、0分钟、0秒、92毫秒

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    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个二级缓存(每核):256 KBL3高速缓存:3兆字节内存:4 GB启动ROM版本:MBP81.0047.B0ESMC版本(系统):1.68F96