【问题标题】:Algorithm to generate anagrams生成字谜的算法
【发布时间】:2010-09-08 11:39:39
【问题描述】:

生成字谜的最佳策略是什么。

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.
  • 十一加二十二加一的字谜
  • 小数点I'm a dot in place 的字谜
  • AstronomersMoon starers 的变位词

起初它看起来很简单,只是将字母打乱并生成所有可能的组合。但是只生成字典中的单词的有效方法是什么。

我看到了这个页面,Solving anagrams in Ruby

但是你的想法是什么?

【问题讨论】:

  • 在期待中安顿下来..!如果您需要输出作为原始短语的线索,我真的不明白您如何“生成”它。当然,您所能做的就是生成一个短语/字谜配对列表并从中挑选?算法如何理解 astronomers=moon starers,例如?
  • 当然,生成好的字谜是一个难题,但生成不好的字谜更容易:)

标签: algorithm language-agnostic puzzle


【解决方案1】:

这些答案中的大多数都非常低效和/或只会给出一个单词的解决方案(没有空格)。我的解决方案可以处理任意数量的单词并且非常高效。

你想要的是一个 trie 数据结构。这是一个完整的 Python 实现。你只需要保存在一个名为words.txt的文件中的单词列表你可以在这里试试Scrabble字典单词列表:

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,则仅使用“astronomers”作为输入会产生 233,549 个结果。也许您可以找到一个更短的单词列表,其中只包含更常见的英语单词。

此外,除非您将“im”添加到字典并将 MIN_WORD_SIZE 设置为 2,否则缩略词“I'm”(来自您的一个示例)不会显示在结果中。

获取多个单词的诀窍是每当您在搜索中遇到一个完整的单词时就跳回树中的根节点。然后你继续遍历 trie,直到所有字母都用完。

【讨论】:

  • 我的字谜程序中字长为 1 的天文学家只有 16818 个字谜,因为它没有给出排列。使用我的 AMD Sempron 不起眼的计算机运行大约 2 秒即可产生结果。我将结果保存到文件中,它比文本控制台中的大量单词更有用。我不使用树结构,而是使用带有递归的纯文本,匹配字典中的键,用排序的字母键进行哈希处理。
  • 我在 DaniWeb 中发布了我之前的代码daniweb.com/software-development/python/code/393153/…
  • 错误报告:如果单词表有两个条目:“foobar”和“foob”(按此顺序),那么代码 sn-p 将找不到“boof”的字谜。只有颠倒单词表的顺序,它才会正确返回“foob”。我认为这可以通过在第一个 for 循环中添加另一个 if 子句来解决,但我必须把它留给了解 Python 的人。
  • 你能用几句话描述你的算法吗?我特别感兴趣的是,当您决定可以使用输入的某些字母组成某个字典单词之后会发生什么。我知道我们然后检查剩余的字符是否可以用来组成其他单词。我们怎么知道我们已经用尽了所有的可能性?
  • @MadPhysicist trie 结构允许您特别利用英语中许多单词相同但结尾不同的情况。因此,如果您的字谜输入字母包含“q”、“u”但不包含“i”,那么只需 3 步,我们就可以消除“quick”、“quickly”、“quicker”、“quicken”等……所以它是一种以实用的方式将单词分组为彼此子集的结构。我怀疑还有另一种数据结构,它还允许您消除所有带有字母“i”的单词,并且不关心字母顺序,但不确定如何保持它的大小易于处理。
【解决方案2】:

对于字典中的每个单词,按字母顺序对字母进行排序。所以“foobar”变成了“abfoor”。

然后当输入的字谜出现时,也对它的字母进行排序,然后查找它。 与哈希表查找一样快!

对于多个单词,您可以对已排序的字母进行组合,随时进行排序。仍然比生成所有组合快得多

(有关更多优化和详细信息,请参阅 cmets)

【讨论】:

  • 看起来这个(连同吉米的回答)只适用于一个单词字谜——这怎么能应用于短语的字谜?
  • 正如我在帖子中所说,对于多个单词,您可以检查所有对、三元组等,在每种情况下结合字母和排序(并使用合并排序,以便运算更快!)并测试反对那个组合。你还可以更聪明,例如完全使用的字符位域和...
  • ...显然是字符总数,所以当我说“测试所有三元组”时,您可以修剪大量类别。例如,首先按长度存储单词,然后按哈希存储。因此,在您的双人/三人组中,您已经能够跳过字符数错误的组合。
  • 排序确实有帮助——这是将有序的字母列表映射到无序列表的最简单方法(除了使用 .NET HashSet 或 Python set())。
  • 好吧,很公平,它加快了速度,因为“foobar”和“barfoo”的字谜将解析为相同的结果集,但如果你只想从一个句子中得到所有字谜,那么排序对您没有帮助,因为您需要考虑所有可用的字符。
【解决方案3】:

请参阅华盛顿大学 CSE 系的 assignment

基本上,您有一个数据结构,它只包含单词中每个字母的计数(数组适用于 ascii,如果您需要 unicode 支持,请升级到地图)。您可以减去其中两个字母组;如果计数为负数,您就知道一个词不能是另一个词的字谜。

【讨论】:

  • 使用计数使其成为简单的组合问题。您有一个搜索短语的映射,并将其与具有相同计数总和的单词映射组合进行匹配。这是一个优雅的解决方案。
【解决方案4】:

预处理:

用每个叶子作为一个已知单词构建一个 trie,按字母顺序键入。

在搜索时:

将输入字符串视为多重集。通过像深度优先搜索一样遍历索引树来找到第一个子词。在每个分支上,您都可以问,我输入的其余部分中是否包含字母 x?如果你有一个好的多集表示,这应该是一个常数时间查询(基本上)。

一旦你有了第一个子词,你就可以保留剩余的多重集并将其视为一个新的输入来查找该字谜的其余部分(如果存在的话)。

通过记忆增强此过程,以便更快地查找常见的余数多重集。

这非常快 - 每次 trie 遍历都保证会给出一个实际的子字,并且每次遍历都会在子字的长度上花费线性时间(根据编码标准,子字通常非常小)。但是,如果您真的想要更快的东西,您可以在预处理中包含所有 n-gram,其中 n-gram 是连续 n 个单词的任何字符串。当然,如果 W = #words,那么您将从索引大小 O(W) 跳转到 O(W^n)。也许 n = 2 是现实的,具体取决于字典的大小。

【讨论】:

    【解决方案5】:

    Michael Morton(Mr. Machine Tool)使用名为 Ars Magna 的工具,对编程字谜进行了开创性的工作。这是a light article 根据他的工作。

    【讨论】:

      【解决方案6】:

      所以here's 是 Jason Cohen 建议的 Java 工作解决方案,它的性能比使用 trie 的解决方案要好一些。以下是一些要点:

      • 仅加载包含给定单词集子集的单词的字典
      • 字典将作为键的已排序单词的哈希值和作为值的实际单词集(如 Jason 所建议)
      • 遍历字典键中的每个单词并执行递归前向查找以查看是否为该键找到任何有效的字谜
      • 只进行正向查找,因为应该已经找到所有已经遍历的单词的字谜
      • 合并所有与键关联的单词,例如如果 'enlist' 是要找到字谜的单词,并且要合并的一组键是 [ins] 和 [elt],而键 [ins] 的实际单词是 [sin] 和 [ins],对于键 [elt] 是 [let],那么最终的合并词集将是 [sin, let] 和 [ins, let],这将是我们最终字谜列表的一部分
      • 还要注意,此逻辑将仅列出唯一的单词集,即“十一加二”和“二加十一”将相同,并且只有其中一个会在输出中列出

      以下是查找字谜键集的主要递归代码:

      // 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 分叉回购并使用它。我可能错过了许多优化。但代码有效并且确实找到了所有的字谜。

      【讨论】:

        【解决方案7】:

        here 是我的新颖解决方案。

        乔恩·本特利 (Jon Bentley) 的书 Programming Pearls 包含一个关于查找单词字谜的问题。 声明:

        给定一本英语单词词典,找出所有的字谜。为了 例如,“pots”、“stop”和“tops”都是彼此的字谜 因为每个都可以通过排列其他字母来形成。

        我想了想,解决方案是获取您正在搜索的单词的签名并将其与字典中的所有单词进行比较。一个单词的所有字谜应该有相同的签名。但是如何实现呢?我的想法是使用算术基本定理:

        算术基本定理表明

        每个正整数(数字 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;
                }
            }
        }
        

        【讨论】:

          【解决方案8】:

          几个月前我使用了以下计算字谜的方法:

          • 为字典中的每个单词计算一个“代码”:创建一个从字母表中的字母到素数的查找表,例如以 ['a', 2] 开始,以 ['z', 101] 结束。作为预处理步骤,通过在查找表中查找它包含的每个字母的质数并将它们相乘来计算字典中每个单词的代码。为以后的查找创建一个代码到单词的多重映射。

          • 如上所述计算输入单词的代码。

          • 为多重映射中的每个代码计算 codeInDictionary % inputCode。如果结果为 0,则您找到了一个字谜,您可以查找相应的单词。这也适用于 2 个或多个单词的字谜。

          希望对您有所帮助。

          【讨论】:

          • 为什么这么复杂的字典...素数、预处理、多重映射?只需将您的字典键设为排序字符串即可。
          • @IgorGanapolsky 因为它本身只能给你一个单词字谜。 “十一加二”的例子不可能作为输出。
          【解决方案9】:

          Jon Bentley 的Programming Pearls 一书很好地涵盖了这类内容。必读。

          【讨论】:

          • 不知道为什么你被修改了,但 Programming Pearls 的第 2 列介绍了一个程序的实现,该程序在给定单词字典的情况下查找所有字谜集。绝对值得一看。编译并运行代码如下: ./sign
          【解决方案10】:

          我是怎么看的:

          你想建立一个表格,将无序的字母集映射到列出的单词,即通过字典,这样你就可以结束了,比如说

          lettermap[set(a,e,d,f)] = { "deaf", "fade" }
          

          然后从你的起始词,你找到一组字母:

           astronomers => (a,e,m,n,o,o,r,r,s,s,t)
          

          然后遍历该组的所有分区(这可能是最技术性的部分,只是生成所有可能的分区),并查找该组字母的单词。

          编辑:嗯,这几乎就是 Jason Cohen 发布的内容。

          编辑:此外,问题上的 cmets 提到生成“好”字谜,例如示例:)。建立所有可能的字谜列表后,通过 WordNet 运行它们并找到语义上接近原始短语的那些:)

          【讨论】:

            【解决方案11】:

            不久前,我写了一篇关于如何快速找到两个单词字谜的博客文章。它的运行速度非常快:在一个 Ruby 程序中,为一个文本文件超过 300,000 个单词(4 兆字节)的单词查找所有 44 个两个单词的字谜只需要 0.6 秒。

            Two Word Anagram Finder Algorithm (in Ruby)

            如果允许将单词列表预处理为从按字母排序的单词到使用这些字母的单词列表的大型哈希映射,则可以使应用程序更快。从此预处理后的数据可以序列化和使用。

            【讨论】:

            • 我删除了我之前的评论,因为它是错误的。无论如何: ("az".sum + "by".sum) - "mmnn".sum => 0。该校验和函数不适合字谜求解
            • 它并不完美,但非常快。您需要对任何校验和进行最终检查,因为冲突的可能性不会消失。
            【解决方案12】:

            如果我将字典作为哈希映射,因为每个单词都是唯一的,并且 Key 是单词的二进制(或十六进制)表示。然后,如果我有一个单词,我可以很容易地以 O(1) 复杂度找到它的含义。

            现在,如果我们必须生成所有有效的字谜,我们需要验证生成的字谜是否在字典中,如果它存在于字典中,则它是有效的,否则我们需要忽略它。

            我假设一个单词最多 100 个字符(或更多,但有限制)。

            所以我们把它当作一个索引序列的任何单词,比如单词“hello”,都可以表示为 “1234”。 现在“1234”的字谜是“1243”,“1242”..等

            我们唯一需要做的就是为特定数量的字符存储所有此类索引组合。这是一项一次性任务。 然后可以通过从索引中选择字符来从组合中生成单词。因此我们得到了字谜。

            要验证字谜是否有效,只需索引字典并验证即可。

            唯一需要处理的是重复项。这很容易做到。当我们需要与之前在字典中搜索过的进行比较时。

            解决方案强调性能。

            【讨论】:

              【解决方案13】:

              在我的脑海中,最有意义的解决方案是从输入字符串中随机挑选一个字母,然后根据以该字母开头的单词过滤字典。然后选择另一个,过滤第二个字母等。此外,过滤掉剩下的文本不能组成的单词。然后当你打到一个单词的结尾时,插入一个空格并用剩下的字母重新开始。你也可以根据词的类型来限制词(例如,你不会有两个相邻的动词,你不会有两个相邻的文章,等等)。

              【讨论】:

                【解决方案14】:
                1. 正如 Jason 建议的那样,准备一个字典来制作哈希表,其中键按字母顺序排列,并为单词本身赋值(每个键可能有多个值)。
                2. 在查找之前删除空格并对查询进行排序。

                在此之后,您需要进行某种递归、详尽的搜索。伪代码很粗略:

                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)
                

                最后,您需要遍历解决方案列表,并打印每个子列表中的单词,它们之间有空格。对于这些情况,您可能需要打印所有订单(例如,“我是 Sam”和“Sam I am”都是解决方案)。

                当然,我没有测试这个,这是一种蛮力方法。

                【讨论】:

                  猜你喜欢
                  • 2011-11-03
                  • 2011-01-17
                  • 2012-11-21
                  • 2012-05-25
                  • 2017-10-28
                  • 2010-12-23
                  • 2010-12-01
                  • 2012-11-03
                  • 2016-11-15
                  相关资源
                  最近更新 更多