生成字谜的最佳策略是什么。
An anagram is a type of word play, the result of rearranging the letters of a word or phrase to produce a new word or phrase, using all the original letters exactly once; ex.
- 11加2 是 十二加一
的字谜- 小数点是 的字谜我是一个点
- 天文学家是 Moon starers
的字谜
起初它看起来很简单,只是混杂字母并生成所有可能的组合。但是,只生成字典中的单词的有效方法是什么。
我遇到了这个页面Solving anagrams in Ruby。
但你的想法是什么?
答案 0 :(得分:43)
这些答案中的大多数都非常低效和/或只能提供单字解决方案(无空格)。我的解决方案将处理任意数量的单词并且非常有效。
你想要的是一个特里数据结构。这是一个完整的 Python实现。您只需要保存在名为words.txt
的文件中的单词列表您可以在此处尝试拼字游戏字典单词列表:
http://www.isc.ro/lists/twl06.zip
MIN_WORD_SIZE = 4 # min size of a word in the output
class Node(object):
def __init__(self, letter='', final=False, depth=0):
self.letter = letter
self.final = final
self.depth = depth
self.children = {}
def add(self, letters):
node = self
for index, letter in enumerate(letters):
if letter not in node.children:
node.children[letter] = Node(letter, index==len(letters)-1, index+1)
node = node.children[letter]
def anagram(self, letters):
tiles = {}
for letter in letters:
tiles[letter] = tiles.get(letter, 0) + 1
min_length = len(letters)
return self._anagram(tiles, [], self, min_length)
def _anagram(self, tiles, path, root, min_length):
if self.final and self.depth >= MIN_WORD_SIZE:
word = ''.join(path)
length = len(word.replace(' ', ''))
if length >= min_length:
yield word
path.append(' ')
for word in root._anagram(tiles, path, root, min_length):
yield word
path.pop()
for letter, node in self.children.iteritems():
count = tiles.get(letter, 0)
if count == 0:
continue
tiles[letter] = count - 1
path.append(letter)
for word in node._anagram(tiles, path, root, min_length):
yield word
path.pop()
tiles[letter] = count
def load_dictionary(path):
result = Node()
for line in open(path, 'r'):
word = line.strip().lower()
result.add(word)
return result
def main():
print 'Loading word list.'
words = load_dictionary('words.txt')
while True:
letters = raw_input('Enter letters: ')
letters = letters.lower()
letters = letters.replace(' ', '')
if not letters:
break
count = 0
for word in words.anagram(letters):
print word
count += 1
print '%d results.' % count
if __name__ == '__main__':
main()
运行程序时,单词将加载到内存中的trie中。之后,只需键入要搜索的字母,它就会打印结果。它只会显示使用所有输入字母的结果,不会更短。
它会过滤输出中的短字,否则结果数量会很大。随意调整MIN_WORD_SIZE
设置。请记住,如果MIN_WORD_SIZE
为1,只使用“天文学家”作为输入就会得到233,549个结果。也许你可以找到一个只包含更多常用英语单词的较短单词列表。
此外,除非您在字典中添加“im”并将MIN_WORD_SIZE
设置为2,否则收缩“我是”(来自您的某个示例)将不会显示在结果中。
获取多个单词的技巧是在搜索中遇到完整单词时跳回到trie中的根节点。然后你继续遍历特里直到所有字母都被使用。
答案 1 :(得分:19)
对于字典中的每个单词,按字母顺序对字母进行排序。所以“foobar”变成“abfoor”。
然后当输入的字谜进来时,也要对它的字母进行排序,然后查找它。 它与散列表查找一样快!
对于多个单词,您可以对已排序的字母进行组合,然后进行排序。仍然很多比生成所有组合更快。
(有关更多优化和详细信息,请参阅注释)
答案 2 :(得分:8)
请参阅华盛顿大学CSE部门的assignment。
基本上,你有一个数据结构只包含一个单词中每个字母的数量(一个数组适用于ascii,如果你想要unicode支持,则升级到一个地图)。你可以减去其中两个字母集;如果计数是否定的,你就知道一个单词不能成为另一个单词的字谜。
答案 3 :(得分:4)
预过程:
使用每个叶子构建一个trie作为已知单词,按字母顺序键入。
在搜索时间:
将输入字符串视为多集。通过遍历深度优先搜索中的索引trie来查找第一个子字。在每个分支,你可以问,在我的输入的剩余部分是字母x?如果你有一个很好的multiset表示,这应该是一个恒定时间查询(基本上)。
一旦你有了第一个子词,你就可以保留余数多重集,并将其作为一个新的输入来查找其余的anagram(如果有的话)。
使用memoization扩充此过程,以便在常见的剩余多字节上更快地查找。
这非常快 - 每个trie遍历都保证给出一个实际的子字,并且每个遍历在子字的长度上占用线性时间(并且通过编码标准,子字通常非常小)。但是,如果确实想要更快的东西,则可以在预处理中包含所有n-gram,其中n-gram是连续n个字的任意字符串。当然,如果W = #words,那么你将从索引大小O(W)跳到O(W ^ n)。也许n = 2是现实的,这取决于字典的大小。
答案 4 :(得分:3)
关于程序化字谜的开创性作品之一是由迈克尔莫顿(机械工具先生)使用名为Ars Magna的工具。根据他的工作,这里是a light article。
答案 5 :(得分:3)
所以here's Jason Cohen建议的Java工作解决方案,它的性能比使用trie的解决方案要好一些。以下是一些要点:
下面是查找anagram键集的主要递归代码:
// recursive function to find all the anagrams for charInventory characters
// starting with the word at dictionaryIndex in dictionary keyList
private Set<Set<String>> findAnagrams(int dictionaryIndex, char[] charInventory, List<String> keyList) {
// terminating condition if no words are found
if (dictionaryIndex >= keyList.size() || charInventory.length < minWordSize) {
return null;
}
String searchWord = keyList.get(dictionaryIndex);
char[] searchWordChars = searchWord.toCharArray();
// this is where you find the anagrams for whole word
if (AnagramSolverHelper.isEquivalent(searchWordChars, charInventory)) {
Set<Set<String>> anagramsSet = new HashSet<Set<String>>();
Set<String> anagramSet = new HashSet<String>();
anagramSet.add(searchWord);
anagramsSet.add(anagramSet);
return anagramsSet;
}
// this is where you find the anagrams with multiple words
if (AnagramSolverHelper.isSubset(searchWordChars, charInventory)) {
// update charInventory by removing the characters of the search
// word as it is subset of characters for the anagram search word
char[] newCharInventory = AnagramSolverHelper.setDifference(charInventory, searchWordChars);
if (newCharInventory.length >= minWordSize) {
Set<Set<String>> anagramsSet = new HashSet<Set<String>>();
for (int index = dictionaryIndex + 1; index < keyList.size(); index++) {
Set<Set<String>> searchWordAnagramsKeysSet = findAnagrams(index, newCharInventory, keyList);
if (searchWordAnagramsKeysSet != null) {
Set<Set<String>> mergedSets = mergeWordToSets(searchWord, searchWordAnagramsKeysSet);
anagramsSet.addAll(mergedSets);
}
}
return anagramsSet.isEmpty() ? null : anagramsSet;
}
}
// no anagrams found for current word
return null;
}
您可以从here分叉回购并使用它。我可能错过了许多优化。但是代码可以工作并找到所有的字谜。
答案 6 :(得分:3)
here是我的新颖解决方案。
Jon Bentley的书“珍珠编程”(Programming Pearls)包含了一个关于找到字谜的问题。 声明:
给出一个英文单词词典,找到所有的字谜集。对于 例如,“花盆”,“停止”和“顶部”都是彼此的字谜 因为每一个都可以通过置换其他人的字母来形成。
我想了一下,我觉得解决方法是获取您正在搜索的单词的签名,并将其与字典中的所有单词进行比较。一个单词的所有字谜应该具有相同的签名。但是如何实现这一目标呢?我的想法是使用算术的基本定理:
算术的基本定理表明
每个正整数(数字1除外)都可以表示 除了作为一个或多个产品的重新排列之外,还有一种方式 素数
所以想法是使用前26个素数的数组。然后对于单词中的每个字母,我们得到相应的素数A = 2,B = 3,C = 5,D = 7 ......然后我们计算输入词的乘积。接下来,我们对字典中的每个单词执行此操作,如果单词与输入单词匹配,则将其添加到结果列表中。
性能或多或少可以接受。对于479828个单词的字典,获取所有字谜需要160毫秒。这大约是0.0003毫秒/字,或0.3微秒/字。算法的复杂度似乎是O(mn)或~O(m),其中m是字典的大小,n是输入字的长度。
以下是代码:
package com.vvirlan;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Scanner;
public class Words {
private int[] PRIMES = new int[] { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73,
79, 83, 89, 97, 101, 103, 107, 109, 113 };
public static void main(String[] args) {
Scanner s = new Scanner(System.in);
String word = "hello";
System.out.println("Please type a word:");
if (s.hasNext()) {
word = s.next();
}
Words w = new Words();
w.start(word);
}
private void start(String word) {
measureTime();
char[] letters = word.toUpperCase().toCharArray();
long searchProduct = calculateProduct(letters);
System.out.println(searchProduct);
try {
findByProduct(searchProduct);
} catch (Exception e) {
e.printStackTrace();
}
measureTime();
System.out.println(matchingWords);
System.out.println("Total time: " + time);
}
private List<String> matchingWords = new ArrayList<>();
private void findByProduct(long searchProduct) throws IOException {
File f = new File("/usr/share/dict/words");
FileReader fr = new FileReader(f);
BufferedReader br = new BufferedReader(fr);
String line = null;
while ((line = br.readLine()) != null) {
char[] letters = line.toUpperCase().toCharArray();
long p = calculateProduct(letters);
if (p == -1) {
continue;
}
if (p == searchProduct) {
matchingWords.add(line);
}
}
br.close();
}
private long calculateProduct(char[] letters) {
long result = 1L;
for (char c : letters) {
if (c < 65) {
return -1;
}
int pos = c - 65;
result *= PRIMES[pos];
}
return result;
}
private long time = 0L;
private void measureTime() {
long t = new Date().getTime();
if (time == 0L) {
time = t;
} else {
time = t - time;
}
}
}
答案 7 :(得分:2)
Jon Bentley撰写的 Programming Pearls 这本书很好地涵盖了这种东西。必须阅读。
答案 8 :(得分:1)
我怎么看:
你想要建立一个表格,将无序的字母组映射到列出的单词,即通过字典,这样你就可以了,比如说
lettermap[set(a,e,d,f)] = { "deaf", "fade" }
然后从你的起始单词,你找到一组字母:
astronomers => (a,e,m,n,o,o,r,r,s,s,t)
然后循环遍历该集合的所有分区(这可能是最技术性的部分,只生成所有可能的分区),并查找该组字母的单词。
编辑:嗯,这就是Jason Cohen发布的内容。
编辑:此外,关于这个问题的评论提到生成“好”的字谜,就像例子:)。在您构建所有可能的字谜列表之后,通过WordNet运行它们并找到语义上接近原始短语的那些:)
答案 9 :(得分:1)
几个月前我使用了以下计算字谜的方法:
为字典中的每个单词计算“代码”:从字母表中的字母到素数创建查找表,例如:以['a',2]开头,以['z',101]结尾。作为预处理步骤,通过在查找表中查找每个字母的素数并将它们相乘,计算字典中每个单词的代码。对于以后的查找,请创建代码到字的多图。
计算输入字的代码,如上所述。
为multimap中的每个代码计算codeInDictionary%inputCode。如果结果为0,则表示您找到了一个字谜,您可以查找相应的单词。这也适用于2个或更多单词的字谜。
希望这很有用。
答案 10 :(得分:1)
前段时间我写了一篇关于如何快速找到两个单词字谜的博文。它的工作速度非常快:在一个Ruby程序中,查找文本文件超过300,000字(4兆字节)的单词的所有44个双字字谜只需0.6秒。
Two Word Anagram Finder Algorithm (in Ruby)
当允许将wordlist预处理为使用这些字母从字母排序到单词列表的大型哈希映射时,可以使应用程序更快。从那时起,可以对这些预处理数据进行序列化和使用。
答案 11 :(得分:1)
如果我将字典作为哈希映射,因为每个单词都是唯一的,而密钥是单词的二进制(或十六进制)表示。然后,如果我有一个单词,我可以很容易地找到O(1)复杂度的含义。
现在,如果我们必须生成所有有效的字谜,我们需要验证生成的anagram是否在字典中,如果它存在于字典中,它是一个有效的字符,我们需要忽略它。
我假设可以有一个最多100个字符的单词(或更多,但有一个限制)。
因此,我们把它作为一系列索引(如单词“hello”)的任何单词都可以表示为 “1234”。 现在“1234”的字谜是“1243”,“1242”..等等
我们唯一需要做的就是为特定数量的字符存储所有这些索引组合。这是一次性任务。 然后通过从索引中选择字符,可以从组合中生成单词。因此我们得到了字谜。
要验证字谜是否有效,只需索引字典并验证。
唯一需要处理的是重复项。这可以轻松完成。当我们需要与之前在字典中搜索过的那些进行比较时。
解决方案强调性能。
答案 12 :(得分:0)
在我的脑海中,最有意义的解决方案是随机选择输入字符串中的一个字母,并根据以该字母开头的单词过滤字典。然后选择另一个,过滤第二个字母等。此外,过滤掉剩余文本无法生成的单词。然后当你点击一个单词的结尾时,插入一个空格并用剩余的字母重新开始。你也可以根据单词类型限制单词(例如,你不会有两个动词彼此相邻,你不会有两篇文章彼此相邻,等等。)
答案 13 :(得分:0)
在此之后,您需要进行某种递归,详尽的搜索。伪代码非常粗略:
function FindWords(solutionList, wordsSoFar, sortedQuery)
// base case
if sortedQuery is empty
solutionList.Add(wordsSoFar)
return
// recursive case
// InitialStrings("abc") is {"a","ab","abc"}
foreach initialStr in InitalStrings(sortedQuery)
// Remaining letters after initialStr
sortedQueryRec := sortedQuery.Substring(initialStr.Length)
words := words matching initialStr in the dictionary
// Note that sometimes words list will be empty
foreach word in words
// Append should return a new list, not change wordSoFar
wordsSoFarRec := Append(wordSoFar, word)
FindWords(solutionList, wordSoFarRec, sortedQueryRec)
最后,您需要遍历solutionList,并在每个子列表中打印单词,并在它们之间留出空格。您可能需要打印这些案例的所有订单(例如“我是Sam”和“Sam我是”都是解决方案)。
当然,我没有对此进行测试,这是一种蛮力的方法。