玩儿一下Word Search Puzzle

简介:

单词搜索迷宫(Word Search Puzzle)问题的输入是一个二维的字符数组和一组单词,目标是找出字符数组网格中的所有单词。这些单词可以是水平的、垂直的或者是任意的对角线方向,所以需要查找8个不同的方向。如下图的网格:

  0 1 2 3
0 t h i s
1 w a t s
2 o a h g
3 f g d t

这里面共有4个单词:this([0, 0]-[0, 3])、two([0, 0]-[2, 0])、fat([3, 0]-[1, 2])和that([3, 3]-[0, 0]),其它更短的单词此处没有列出。

问题分析

最直接的方法就是,使用蛮力算法逐一检查每个可能的字符串是否在单词表中,就像我们用眼睛去检查那样,可以描述为:

for each row R 
     for each column C 
          for each direction D 
               check if word W in wordList

假设单词列表wordList有40个单词,16×16的网格,需要大约80000(16*16*8*40)次检查,这种方法还是可行的。但如果wordList扩展到整部字典,比如有40000个单词,那工作量不小了(考虑到每个方向上,可能的单词都会有多个;而且每次都要在40000个单词里查找)。此时再采用线性查找,就不靠谱了,所以考虑将wordList按字典顺序存储,采用二分查找,这样对于每个可能的单词最多16次比较就可以了。

进一步观察,假定某方向上检查到字符串qx,词典里没有以qx开头的单词,此时可以确定这个方向不需要进一步检查了。

剩下的问题是,怎样检查8个方向的单词呢?来看位于[0, 0]处的字符t(这里忽略掉所有的单字符单词,所以一个字符本身不会构成单词),向右检查,终点的位置分别是[0, 1],[0, 2],[0, 3],即行索引不变,列索引递增;向右下方向检查,终点的位置分别是[1, 1],[2, 2],[3, 3],即行索引、列索引同时递增。每个方向都是如此,这样我们找出每个方向上行索引、列索引的变化规律来,就容易实现了。

实现

现在通过上面分析到的二分查找、前缀检查和方向检查,基本上可以按思路直接写出来。创建类WordSearchPuzzle来完成功能,它的基本方法有:

WordSearchPuzzle

在构造函数里,读取puzzle网格board和单词列表words,然后通过Solve方法进行检查,它会借助于SolveDirection和PrefixSearch方法。Solve的代码是:

复制代码

  
  
public int Solve()
{
int matches = 0 ;
for ( int rowIndex = 0 ; rowIndex < rows; rowIndex ++ )
{
for ( int colIndex = 0 ; colIndex < columns; colIndex ++ )
{
for ( int rd = - 1 ; rd <= 1 ; rd ++ )
{
for ( int cd = - 1 ; cd <= 1 ; cd ++ )
{
if (rd != 0 || cd != 0 )
{
matches
+= SolveDirection(rowIndex, colIndex, rd, cd);
}
}
}
}
}

return matches;
}
复制代码

这里通过rd和cd来表示行和列的变化方式,如当rd=0,cd=1,表示向右检查;当rd=-1,cd=-1,表示向左上方检查。rd和cd的取值都是-1、0、1,共有9中组合,除了(0, 0),正好可以表示8个方向。

SolveDirection返回在指定位置的某个方向上找到了几个单词:

复制代码

  
  
private int SolveDirection( int baseRow, int baseCol, int rowDelta, int colDelta)
{
string charSequence = theBoard[baseRow, baseCol].ToString();
int matches = 0 ;
int searchResult = 0 ;

for ( int i = baseRow + rowDelta, j = baseCol + colDelta;
i
>= 0 && j >= 0 && i < rows && j < columns;
i
+= rowDelta, j += colDelta)
{
charSequence
+= theBoard[i, j];
searchResult
= PrefixSearch(words, charSequence);

if (searchResult < 0 || searchResult >= words.Length)
{
// No word with this prefix
break ;
}

if (words[searchResult].Equals(charSequence))
{
matches
++ ;
Console.WriteLine(
" Found '{0}' at [{1}, {2}] to [{3}, {4}] " ,
charSequence, baseRow, baseCol, i, j);
}
}

return matches;
}
复制代码

通过for循环来检查这个方向上的所有单词,这里使用了前面提到的前缀检查,来避免不必要的查找。

对于PrefixSearch来说,虽然从名字来看,它查找的是前缀,但也包含了完全匹配的情况:


  
  
private static int PrefixSearch( string [] a, string x)
{
int index = Array.BinarySearch(a, x, new StringPrefixComparer());
return index;
}

这里调用了Array.BinarySearch方法,如果不提供一个comparer,它会使用默认的字符串比较,所以这里创建一个StringPrefixComparer:
复制代码

  
  
internal class StringPrefixComparer : IComparer < string >
{
public int Compare( string x, string y)
{
if (x.StartsWith(y))
{
return 0 ;
}
else
{
return x.CompareTo(y);
}
}
}
复制代码

如果以此为前缀的单词不存在,直接结束循环;如果以此为前缀的单词存在,就顺便检查下该前缀是否恰好匹配一个单词。至此,这个小puzzle就完成了。类的完整代码在这里

单词列表文件里包含下面几个单词:

Word List

可以这样调用Solve方法:


  
  
WordSearchPuzzle puzzle = new WordSearchPuzzle( " puzzle.txt " , " words.txt " );
puzzle.Solve();

那么对于上面的网格,返回结果是:

  
  
Found 'this' at [0, 0] to [0, 3]
Found 'two' at [0, 0] to [2, 0]
Found 'fat' at [3, 0] to [1, 2]
Found 'that' at [3, 3] to [0, 0]

Array.BinarySearch方法

实际上,上面的StringPrefixComparer是可以省略的。直接使用Array.BinarySearch本身就够了。将SolveDirection和PrefixSearch改为:

复制代码

  
  
private static int PrefixSearch( string [] a, string x)
{
int index = Array.BinarySearch(a, x);
return (index >= 0) ? index : ~ index;
}

private int SolveDirection( int baseRow, int baseCol, int rowDelta, int colDelta)
{
string charSequence = board[baseRow, baseCol].ToString();
int matches = 0 ;
int searchResult = 0 ;

for ( int i = baseRow + rowDelta, j = baseCol + colDelta;
i
>= 0 && j >= 0 && i < rows && j < columns;
i
+= rowDelta, j += colDelta)
{
charSequence
+= board[i, j];
searchResult
= PrefixSearch(words, charSequence);

// No words with this prefix.
if (searchResult == words.Length)
{
break;
}
if (!words[searchResult].StartsWith(charSequence))
{
break
;
}

if (words[searchResult].Equals(charSequence))
{
matches
++ ;
Console.WriteLine(
" Found '{0}' at [{1}, {2}] to [{3}, {4}] " ,
charSequence, baseRow, baseCol, i, j);
}
}

return matches;
}
复制代码

也可以完成功能。原因是对于Array.BinarySearch方法来说,如果没有查找到,它不会直接返回-1。看上面的单词列表,如果查找this,会返回5,因为它刚好有匹配值;如果查找th,将返回-5,这个-5是列表中第一个大于th的项(that)的索引(4)的按位取补;如果查找的是wt,则返回-10,wt大于列表中所有项,-11即最后一项的索引(9)加一的按位取补。这样如果BinarySearch的结果是[0-9],说明找到了匹配值;如果小于零,则按位取补返回,取补的结果为列表长度,说明该字符串大于每个单词,不可能有匹配,返回,否则检查该字符串是否为前缀。

以前没有注意到Array.BinarySearch的这个特性,原来它还有此用处:)

好了,现在可以放松一下,玩玩游戏了。

参考:

数据结构与问题求解-Java语言描述(第3版)


本文转自一个程序员的自省博客园博客,原文链接:http://www.cnblogs.com/anderslly/archive/2011/03/20/word-search-puzzle.html,如需转载请自行联系原作者。

目录
相关文章
|
索引
LeetCode 79. Word Search
给定一个2D板和一个单词,找出该单词是否存在于网格中。 该单词可以由顺序相邻的单元的字母构成,其中“相邻”单元是水平或垂直相邻的单元。 相同的字母单元格不得多次使用。
76 0
LeetCode 79. Word Search
LeetCode 212. Word Search II
给定一个二维网格 board 和一个字典中的单词列表 words,找出所有同时在二维网格和字典中出现的单词。 单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。
79 0
LeetCode 212. Word Search II
|
Java 程序员 开发者
LeetCode第三题(Longest Substring Without Repeating Characters)三部曲之三:两次优化
本文是《LeetCode第三题(Longest Substring Without Repeating Characters)三部曲》的第二篇,前一篇文章已经列出了完整的解题思路,今天来将此思路转化为具体的Java代码
104 0
LeetCode第三题(Longest Substring Without Repeating Characters)三部曲之三:两次优化
HDOJ/HDU 1075 What Are You Talking About(字符串查找翻译~Map)
HDOJ/HDU 1075 What Are You Talking About(字符串查找翻译~Map)
140 0
大声说出你对女神的爱!Geek is A choice. Girls make difference.
女王节来了,我们采访了来自于阿里云智能一线的6位geek girl,用两天的时间近距离观察她们快乐工作的,还在银泰百货的支持下绽放她们认真生(chou)活(mei)的光芒。 雏恬 我不想做被保护的女生,我想做改变世界的极客。
转:肉饼的自白:You&#39;ve got to find what you love
《You've got to find what you love》是乔布斯2005年在斯坦福大学毕业典礼上的演讲,当我第一次看到这个演讲视频的时候,被彻底震住了。回顾自己跌跌撞撞的人生道路,就是一个不断寻找然后坚持自己钟爱事业的过程。
1375 0
|
算法
算法学习之路|Play On Words(欧拉道路+dfs)
把单词相接转换成26字母间的有向图,问题就转化成求有向图是否构成欧拉道路。
1409 0