惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hacker News: Front Page
P
Palo Alto Networks Blog
T
ThreatConnect
Apple Machine Learning Research
Apple Machine Learning Research
博客园_首页
T
True Tiger Recordings
P
Privacy & Cybersecurity Law Blog
B
Blog
IT之家
IT之家
Last Week in AI
Last Week in AI
F
Full Disclosure
Hacker News: Ask HN
Hacker News: Ask HN
C
Comments on: Blog
Microsoft Azure Blog
Microsoft Azure Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Microsoft Security Blog
Microsoft Security Blog
博客园 - 【当耐特】
N
News and Events Feed by Topic
NISL@THU
NISL@THU
腾讯CDC
雷峰网
雷峰网
Security Latest
Security Latest
李成银的技术随笔
M
Microsoft Research Blog - Microsoft Research
L
LangChain Blog
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Check Point Blog
Y
Y Combinator Blog
Recent Announcements
Recent Announcements
博客园 - Franky
N
News | PayPal Newsroom
V
V2EX
A
About on SuperTechFans
The Register - Security
The Register - Security
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google Online Security Blog
Google Online Security Blog
MyScale Blog
MyScale Blog
Cisco Talos Blog
Cisco Talos Blog
Vercel News
Vercel News
WordPress大学
WordPress大学
C
Cyber Attacks, Cyber Crime and Cyber Security
The Hacker News
The Hacker News
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
爱范儿
爱范儿
A
Arctic Wolf
L
LINUX DO - 最新话题
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

博客园 - Anders Cui

jieba.NET与Lucene.Net的集成 jieba中文分词的.NET版本:jieba.NET 迷人的斐波那契数 - Anders Cui 趣题一则:交替放置的碟子 趣题一则:冯·诺依曼邻居问题 - Anders Cui 趣题一则:寻找那扇门 HashSet的实现(下) HashSet的实现(上) 趣题一则:如何快速过桥? 制作自己的wikibook 整数的展开 离散数学拾趣(三):集合的子集有多少个 - Anders Cui 找零钱的两种方法 离散数学拾趣(二):逻辑难题 离散数学拾趣(一) 关于命名中的数量和人称 扩展方法浅谈 意外之喜 最近遭遇的两个VS配置
玩儿一下Word Search Puzzle
Anders Cui · 2011-03-20 · via 博客园 - Anders Cui

单词搜索迷宫(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

above
fat
flow
test
that
this
tweak
two
wcg
wow

可以这样调用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版)