与通配符的拼字游戏单词发现者

时间:2011-09-14 15:25:01

标签: c# mysql regex

我遇到了一个问题,似乎有些人遇到过类似问题,但我找不到适合我的解决方案。

我目前正在使用C#,MySQL,HTML5和Javascript构建移动Web应用程序。该应用程序将用于帮助用户在玩Scrabble等游戏时找到可玩的单词。

我遇到的问题: 如何从包含用户字母输入字典的MySQL数据库中获取正确的单词?

更多细节: - 用户可以输入任意数量的字母,也可以使用通配符(代表任何字母)。 - 如果用户输入“TEST”,则结果不能包含超过1 E和S的单词以及超过2 T的单词,其中包含“TESTER”的结果将是错误的。 - 结果不能包含字母数多于输入的字词。

更新:似乎Trie是Eric Lippert here建议的解决我问题的方法。
问题是我是C#和MySQL的初学者,所以这里有一些后续问题:

  1. 如何从MySQL词典创建Trie? (400k +字)
  2. 如何存储Trie以便快速和将来访问?
  3. 如何使用C#访问Trie并从中提取单词?
  4. 非常感谢您的帮助!

3 个答案:

答案 0 :(得分:23)

  

如何从包含用户字母输入字典的MySQL数据库中获取正确的单词?

你没有。关系数据库表不是一个合适的数据结构,可以根据需要有效地解决这个问题。

您所做的是从字典中构建 trie 数据结构(或者,如果您真的是buff,则构建 dawg - 有向无环字图 - 这是一种压缩特里图。)

一旦你有了trie / dawg,在字典中针对给定的机架测试每个单词变得非常便宜,因为你可以“删除”字典的整个巨大分支,而机架不能可能匹配。

让我们看一个小例子。假设您有字典“OP,OPS,OPT,OPTS,POT,POTS,SOP,SOPS,STOP,STOPS”从中构建此trie :(带有$的节点是标记为“word can end here”的节点)

           ^root^
           /  |  \
         O    P    S
         |    |   / \
         P$   O  O   T   
        / \   |  |   |
       T$  S$ T$ P$  O
       |      |  |   |
       S$     S$ S$  P$
                     |
                     S$

你有机架“OPS” - 你做什么?

首先你说“我可以沿着O分支走吗?”是的你可以。所以现在问题是将“PS”与O分支相匹配。你可以沿着P支柱下去吗?是。它有一个单词结束标记吗?是的,所以OP是一个匹配。现在问题是将“S”与OP分支匹配。你可以去T分店吗?不,你可以去S分店吗?是。现在你有了空架子,你必须将它与OPS分支相匹配。它有一个单词结束标记吗?是!因此OPS也匹配。现在回溯到根目录。

你可以去P分店吗?是。现在的问题是将OS与P分支相匹配。沿着PO分支向下并匹配S - 失败。回溯到根。

再次,你看到这是怎么回事。最后,我们走下SOP分支,找到SOP的结尾,所以“SOP”与这个机架相匹配。我们不会去ST分支,因为我们没有T.

我们在字典中尝试了所有可能的单词,发现OP,OPS和SOP都匹配。但我们从来没有调查OPTS,POTS,STOP或STOPS,因为我们没有T.

您看到这种数据结构如何提高效率?一旦确定您没有机架上的字母来创建单词的开头,您就不必调查任何字典单词开始。如果你有PO而没有T,你不必调查POTSHERD或POTATO或POTASH或POTLATCH或POTABLE;所有那些昂贵且毫无结果的搜索都会很快消失。

使系统适应处理“野外”瓷砖非常简单;如果你有OPS ?,那么只需在OPSA,OPSB,OPSC上运行搜索算法26次......它应该足够快,这样做26次便宜(或者如果你有两个空白则做26 x 26次)。 )

这是专业Scrabble AI程序使用的基本算法,当然它们还必须处理诸如电路板位置,机架管理等问题,这会使算法有些复杂化。这个简单的算法版本足够快,可以在机架上生成所有可能的单词。

不要忘记,如果字典没有随时间变化,您只需要计算一次的trie / dawg 。从字典中构建trie可能非常耗时,因此您可能希望一次然后找出一些方法以便以重建的形式将trie存储在磁盘上它很快从磁盘上传来。

您可以通过构建一个DAWG来优化内存使用量。注意有很多重复,因为在英语中,很多单词结束相同,就像很多单词开始一样。 trie在开始时很好地共享节点,但在最后分享它们是一项糟糕的工作。你可以注意到例如“没有孩子的S $”模式是非常常见的,并将trie变为:

           ^root^
          / |  \
        O   P    S
        |   |   / \
        P$  O  O   T   
       /  \ |  |   |
      T$  | T$ P$  O
      |    \ | |   |
       \    \| /   P$
        \    |/    |
         \   |    /
          \  |   /  
           \ |  /
            \| /  
             |/
             |       
             S$

保存一堆节点。然后您可能会注意到两个单词现在以O-P $ -S $结尾,两个单词以T $ -S $结尾,因此您可以将其进一步压缩为:

           ^root^
           / | \
          O  P  S
          |  | / \
          P$ O \  T   
         /  \|  \ |
         |   |   \|
         |   |    O
         |   T$   |
          \  |    P$
           \ |   /
            \|  /  
             | /
             |/   
             S$

现在我们为这本词典提供了最小的DAWG。

进一步阅读:

http://dl.acm.org/citation.cfm?id=42420

http://archive.msdn.microsoft.com/dawg1

http://www.gtoal.com/wordgames/scrabble.html

答案 1 :(得分:0)

以下是我将如何解决问题(假设您当然可以控制数据库,并且可以修改表/添加表,甚至可以控制数据库的原始负载)。

我的解决方案将使用2个表 - >一个表只是您字典中每个可能的字母组合的列表,其中组件字母按字母顺序排序。 (IE TEST将是ESTT,TESTER将是ERSTT,DAD将是ADD)。

第二个表将包含每个单词和对表一的键的引用。

表一 - LetterInWord

Index Letters
1     ESTT
2     ESTTER
3     EST
4     ADD
5     APST

在表1中,您按字母顺序插入单词 - test成为estt

表二 - 单词

Index LetterInWordIndex  Word
1     1                  TEST
2     2                  TESTER
3     3                  SET
4     4                  ADD
5     4                  DAD
6     5                  SPAT
7     5                  PAST               

在表2中,您插入带有适当的单词和索引引用的单词。

这将是一对多的关系 - > LetterInWord表中的一个条目可以在Words表中有多个条目

非外卡查询: 说我的输入字母是SETT 按字母顺序排序。

然后在查找中,从LetterInWord中选择所有“Letters”,其中Letters = value并加入表Words - 您在一个查询中的输出是仅包含这些字母的所有单词的列表

现在为外卡: 说我输入的字母是EST * 记住长度 - 4 剥去通配符 - 你得到EST(确保你按字母顺序排序) 现在查看Letters包含EST和字母长度< = 4加入Words表

的所有情况

那将返回TEST,REST,SET等

我不确定这是否是最有效的方法,但它确实有效。我过去曾用它来从字典中进行单词查找,并且它具有合理的性能和最小的复杂性。

答案 2 :(得分:-1)

如果您拥有的只是字典,那将很难做到。如果您能够制作新表或新列,我会:

创建一个包含该列的列的表,以及26列(每个字母一列) 运行存储的proc / backend进程,计算单词中每个字母的出现次数,并将它们放入相应的列中。

然后(忽略通配符)你可以做

从词典中选择单词,其中tcount< = 2且ecount< = 1且scount< = 1

对于你可以做的通配符和长度< = number_of_letters

实际上总是使用length子句,因为您可以对其进行索引以提高性能。

在查询期间,其他任何事情都会异常缓慢