获取所有子串(拼字游戏)的字谜的所有单词列表的算法?

Posted

技术标签:

【中文标题】获取所有子串(拼字游戏)的字谜的所有单词列表的算法?【英文标题】:Algorithm to get a list of all words that are anagrams of all substrings (scrabble)? 【发布时间】:2010-10-27 04:54:19 【问题描述】:

例如,如果输入字符串是 helloworld,我希望输出如下:

do
he
we
low
hell
hold
roll
well
word
hello
lower
world
...

一直到最长的单词,它是 helloworld 子字符串的字谜。例如在 Scrabble 中。 输入字符串可以是任意长度,但很少超过 16 个字符。

我已经进行了搜索并想出了类似 trie 的结构,但我仍然不确定如何实际执行此操作。

【问题讨论】:

在上述情况下,“do”为什么是一个有效的字符串?它不是任何子串 ryt 的字谜? @nitish712 "do" 有效,因为字母 'd' 和 'o' 在输入字符串中。 【参考方案1】:

用于保存有效条目字典的结构将对效率产生巨大影响。将其组织为一棵树,根是单数零字母“单词”,即空字符串。 root 的每个子节点是一个可能单词的单个第一个字母,这些子节点是可能单词的第二个字母,等等,每个节点都标记为它是否真的构成一个单词。

您的测试器函数将是递归的。它以零个字母开头,从有效条目树中找到“”不是一个单词但它确实有孩子,所以你递归地调用你的测试器,你的起始词(没有字母)附加你的每个可用的剩余字母输入字符串(当时所有的字符串)。检查树中的每个单字母条目,如果有效,请记下;如果是孩子,则重新调用附加每个剩余可用字母的测试器函数,依此类推。

例如,如果您的输入字符串是“helloworld”,您将首先使用“”调用递归测试器函数,将剩余的可用字母“helloworld”作为第二个参数传递。函数看到“”不是单词,但子“h”确实存在。所以它用“h”和“elloworld”来称呼自己。函数看到“h”不是一个词,但存在子“e”。所以它用“he”和“lloworld”来称呼自己。函数看到标记了“e”,所以“he”是一个单词,注意。此外,子“l”存在,因此下一个调用是“hel”和“loworld”。接下来它会找到“hell”,然后是“hello”,然后必须退出,可能接下来会找到“hollow”,然后再次返回到空字符串,然后从“e”字开始。

【讨论】:

我喜欢这个解释。我(几乎)可以理解它。我认为需要进行笔和纸分析。 看起来很接近 Norman Ramsey 的 DAWG(其他帖子);应该知道它有一个正式的定义。 不...我考虑过这一点并决定我不了解测试仪功能。我了解如何构建树。它是如何从“你好”变成“空心”的?递归从来都不是我的强项! 它不会直接从“hello”变成“hollow”。接收到“h”和“elloworld”参数的调用随后将调用所有子节点,这些子节点都是以单词开头的2个字母组合(“he”和“lloworld”;“ho”和“ellworld”;但不是“hl”和任何东西,因为那个孩子不会存在)。通过“he”等调用将到达“hello”,最终递归将耗尽自身。然后再次从“h”和“elloworld”调用“ho”和“ellworld”,该调用最终将递归到达“hollow” 如何使用 SQL 数据库做到这一点?我要使用的字典大小无法由我使用的语言(php)处理。【参考方案2】:

我无法抗拒自己的实现。它通过按字母顺序对所有字母进行排序,并将它们映射到可以从中创建的单词来创建字典。这是一个 O(n) 启动操作,无需查找所有排列。您可以将字典实现为另一种语言的 trie,以实现更快的加速。

“getAnagrams”命令也是一个 O(n) 操作,它在字典中搜索每个单词以查看它是否是搜索的子集。在我的笔记本电脑上执行 getAnagrams("radiotelegraphically")"(一个 20 个字母的单词)大约需要 1 秒钟,并返回 1496 个字谜。

# Using the 38617 word dictionary at 
# http://www.cs.umd.edu/class/fall2008/cmsc433/p5/Usr.Dict.Words.txt
# Usage: getAnagrams("helloworld")

def containsLetters(subword, word):
    wordlen = len(word)
    subwordlen = len(subword)

    if subwordlen > wordlen:
        return False

    word = list(word)
    for c in subword:
        try:
            index = word.index(c)
        except ValueError:
            return False
        word.pop(index)
    return True

def getAnagrams(word):
    output = []
    for key in mydict.iterkeys():
        if containsLetters(key, word):
            output.extend(mydict[key])

    output.sort(key=len)
    return output

f = open("dict.txt")
wordlist = f.readlines()
f.close()

mydict = 
for word in wordlist:
    word = word.rstrip()
    temp = list(word)
    temp.sort()
    letters = ''.join(temp)

    if letters in mydict:
        mydict[letters].append(word)
    else:
        mydict[letters] = [word]

运行示例:

>>> getAnagrams("helloworld")
>>> ['do', 'he', 'we', 're', 'oh', 'or', 'row', 'hew', 'her', 'hoe', 'woo', 'red', 'dew', 'led', 'doe', 'ode', 'low', 'owl', 'rod', 'old', 'how', 'who', 'rho', 'ore', 'roe', 'owe', 'woe', 'hero', 'wood', 'door', 'odor', 'hold', 'well', 'owed', 'dell', 'dole', 'lewd', 'weld', 'doer', 'redo', 'rode', 'howl', 'hole', 'hell', 'drew', 'word', 'roll', 'wore', 'wool','herd', 'held', 'lore', 'role', 'lord', 'doll', 'hood', 'whore', 'rowed', 'wooed', 'whorl', 'world', 'older', 'dowel', 'horde', 'droll', 'drool', 'dwell', 'holed', 'lower', 'hello', 'wooer', 'rodeo', 'whole', 'hollow', 'howler', 'rolled', 'howled', 'holder', 'hollowed']

【讨论】:

这当然可以完成工作!但约翰皮里的方法可能更有效?我会玩两个。 @ 20 世纪男孩:John Pirie 描述了某种特里树,我已经建议将其作为哈希表的可能替代品。然而,这听起来不完整。他描述了与 H-e 一起走的树,它给出了“he”这个词,但忽略了排列“eh”(我认为这是一个词)。 @Unknown:我认为“eh”会在所有“h”词之后被检测到,即当函数回溯到树的顶部并移动到以“e”开头的分支时。 【参考方案3】:

您想要的数据结构称为Directed Acyclic Word Graph (dawg),它由 Andrew Appel 和 Guy Jacobsen 在他们的论文“世界上最快的拼字游戏”中描述,遗憾的是他们选择不在线免费提供。 ACM 会员或大学图书馆将为您提供。

我已经用至少两种语言实现了这个数据结构——它简单、易于实现,而且非常非常快。

【讨论】:

我在谷歌搜索的第一页找到了它:-) 也许有更快的...见ericsink.com/downloads/faster-scrabble-gordon.pdf 感谢@Norman 和 Steve 指出这些惊人的资源 :) GADDAG 是一种更快的生成拼字游戏动作的算法,但不一定只用于查找字谜/子字谜。它们应该具有可比性,只是 DAWG 可能更快,因为它小到可以放入高速缓存中。【参考方案4】:

一种简单的方法是生成所有“子字符串”,并为每个“子字符串”检查它是否是一组可接受单词的元素。例如,在 Python 2.6 中:

import itertools
import urllib

def words():
  f = urllib.urlopen(
    'http://www.cs.umd.edu/class/fall2008/cmsc433/p5/Usr.Dict.Words.txt')
  allwords = set(w[:-1] for w in f)
  f.close()
  return allwords

def substrings(s):
  for i in range(2, len(s)+1):
    for p in itertools.permutations(s, i):
      yield ''.join(p)

def main():
  w = words()
  print '%d words' % len(w)
  ss = set(substrings('weep'))
  print '%d substrings' % len(ss)
  good = ss & w
  print '%d good ones' % len(good)
  sgood = sorted(good, key=lambda w:(len(w), w))
  for aword in sgood:
    print aword

main()

会发出:

38617 words
31 substrings
5 good ones
we
ewe
pew
wee
weep

当然,正如其他回复所指出的那样,有目的地组织数据可以大大加快运行时间——尽管对于快速字谜查找器而言,最佳数据组织可能会有所不同......但这在很大程度上取决于性质您的允许单词字典(数万,例如这里 - 或数百万?)。应该考虑哈希映射和“签名”(基于对每个单词中的字母进行排序),以及尝试等。

【讨论】:

这适用于一个小的测试字符串,但一个 16 字符的字符串会产生 20922789888000 个可能的子字符串。对于长的测试字符串,你希望被有效的条目约束,而不是可能的字母组合。【参考方案5】:

您想要的是power set 的实现。

还可以查看 Eric Lipparts 的博客,他不久前在博客中提到了 this very thing

编辑:

这是我编写的从给定字符串中获取幂集的实现...

private IEnumerable<string> GetPowerSet(string letters)

  char[] letterArray = letters.ToCharArray();
  for (int i = 0; i < Math.Pow(2.0, letterArray.Length); i++)
  
    StringBuilder sb = new StringBuilder();
    for (int j = 0; j < letterArray.Length; j++)
    
      int pos = Convert.ToInt32(Math.Pow(2.0, j));
      if ((pos & i) == pos)
      
        sb.Append(letterArray[j]);
      
    
    yield return new string(sb.ToString().ToCharArray().OrderBy(c => c).ToArray());
  

这个函数为我提供了构成传入字符串的字符的幂集,然后我可以将它们用作字谜字典的键...

Dictionary<string,IEnumerable<string>>

我像这样创建了我的字谜词典...(可能有更有效的方法,但使用拼字游戏单词列表足够简单且足够快)

wordlist = (from s in fileText.Split(new string[]  Environment.NewLine , StringSplitOptions.RemoveEmptyEntries)
                let k = new string(s.ToCharArray().OrderBy(c => c).ToArray())
                group s by k).ToDictionary(o => o.Key, sl => sl.Select(a => a));

【讨论】:

【参考方案6】:

喜欢Tim J、Eric Lippert 的博文是我首先想到的地方。我想补充一点,他写了一篇关于如何提高他第一次尝试的表现的后续文章。

A nasality talisman for the sultana analyst Santalic tailfans, part two

【讨论】:

【参考方案7】:

我相信this question 答案中的 Ruby 代码也能解决您的问题。

【讨论】:

【参考方案8】:

我最近在手机上玩了很多 Wordfeud,我很好奇我是否能想出一些代码来给我一个可能的单词列表。以下代码采用您的可用源字母(* 表示通配符)和一个包含允许单词(TWL、SOWPODS 等)主列表的数组,并生成一个匹配列表。它通过尝试从源字母构建主列表中的每个单词来做到这一点。

我是在写完代码后才发现这个话题的,肯定不如John Pirie的方法或者DAWG算法那么高效,但是还是挺快的。

public IList<string> Matches(string sourceLetters, string [] wordList)

    sourceLetters = sourceLetters.ToUpper();

    IList<string> matches = new List<string>();

    foreach (string word in wordList)
    
        if (WordCanBeBuiltFromSourceLetters(word, sourceLetters))
            matches.Add(word);
    

    return matches;



public bool WordCanBeBuiltFromSourceLetters(string targetWord, string sourceLetters)

    string builtWord = "";

    foreach (char letter in targetWord)
    
        int pos = sourceLetters.IndexOf(letter);
        if (pos >= 0)
        
            builtWord += letter;
            sourceLetters = sourceLetters.Remove(pos, 1);
            continue;
        


        // check for wildcard
        pos = sourceLetters.IndexOf("*");
        if (pos >= 0)
        
            builtWord += letter;
            sourceLetters = sourceLetters.Remove(pos, 1);
        


    

    return string.Equals(builtWord, targetWord);


【讨论】:

以上是关于获取所有子串(拼字游戏)的字谜的所有单词列表的算法?的主要内容,如果未能解决你的问题,请参考以下文章

拼字游戏字谜生成器

在图像中查找拼字游戏板的角点

如何在 javascript 中创建一个单词的所有可能字谜的列表?

优化子串字谜比较算法

文件中的所有字谜

代码高尔夫:查找所有字谜