生成字谜的算法

时间:2008-09-10 20:15:17

标签: algorithm language-agnostic puzzle

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

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

但你的想法是什么?

14 个答案:

答案 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的解决方案要好一些。以下是一些要点:

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

下面是查找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)

  1. 正如Jason建议的那样,准备一个字典制作哈希表,其中键是按字母顺序排序的,并且值字本身(每个键可能有多个值)。
  2. 删除空格并在查找之前对查询进行排序。
  3. 在此之后,您需要进行某种递归,详尽的搜索。伪代码非常粗略:

    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我是”都是解决方案)。

    当然,我没有对此进行测试,这是一种蛮力的方法。