高效搜索乱码中的单词

时间:2012-01-19 10:04:13

标签: python pyenchant

我想你可以把它归类为拼字游戏风格的问题,但它起初是因为一位朋友提到了英国电视智力竞赛节目Countdown。在节目中的各种轮次涉及参赛者被提出一组乱七八糟的字母,他们必须提出他们可以用的最长的单词。我朋友提到的那个是“RAEPKWAEN”。

在相当短的时间内,我用Python编写了一些东西来处理这个问题,使用PyEnchant来处理字典查找,但是我注意到它确实无法很好地扩展。

这是我目前所拥有的:

#!/usr/bin/python

from itertools import permutations
import enchant
from sys import argv

def find_longest(origin):
    s = enchant.Dict("en_US")
    for i in range(len(origin),0,-1):
        print "Checking against words of length %d" % i
        pool = permutations(origin,i)
        for comb in pool:
            word = ''.join(comb)
            if s.check(word):
                return word
    return ""

if (__name__)== '__main__':
    result = find_longest(argv[1])
    print result

这就像他们在节目中使用的9个字母的例子一样好,9 factorial = 362,880和8 factorial = 40,320。在这个等级上,即使它必须检查所有可能的排列和字长,也不是那么多。

然而,一旦你达到了14个字符,那就是87,178,291,200可能的组合,这意味着你很快就能找到一个14个字符的单词。

通过上面的示例单词,我的机器大约需要12 1/2秒才能找到“重新唤醒”。有了14个字符的混乱单词,我们可以在23天的范围内进行讨论,只是为了检查所有可能的14个字符排列。

有没有更有效的方法来解决这个问题?

10 个答案:

答案 0 :(得分:5)

Jeroen Coupé his answer来自{{3}}字母计数的实现:

from collections import defaultdict, Counter


def find_longest(origin, known_words):
    return iter_longest(origin, known_words).next()

def iter_longest(origin, known_words, min_length=1):
    origin_map = Counter(origin)
    for i in xrange(len(origin) + 1, min_length - 1, -1):
        for word in known_words[i]:
            if check_same_letters(origin_map, word):
               yield word

def check_same_letters(origin_map, word):
    new_map = Counter(word)
    return all(new_map[let] <= origin_map[let] for let in word)

def load_words_from(file_path):
    known_words =  defaultdict(list)
    with open(file_path) as f:
        for line in f:
            word = line.strip()
            known_words[len(word)].append(word)
    return known_words

if __name__ == '__main__':
    known_words = load_words_from('words_list.txt')
    origin = 'raepkwaen'
    big_origin = 'raepkwaenaqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnm'
    print find_longest(big_origin, known_words)
    print list(iter_longest(origin, known_words, 5))

输出(对于我的小58000字dict):

counterrevolutionaries
['reawaken', 'awaken', 'enwrap', 'weaken', 'weaker', 'apnea', 'arena', 'awake',
 'aware', 'newer', 'paean', 'parka', 'pekan', 'prank', 'prawn', 'preen', 'renew',
 'waken', 'wreak']

注意:

  • 这是没有优化的简单实现。

  • words_list.txt - 在Linux上可以是/usr/share/dict/words

<强>更新

如果我们只需要查找一次单词,并且我们的词典中包含按长度排序的单词,例如通过这个脚本:

with open('words_list.txt') as f:
    words = f.readlines()
with open('words_by_len.txt', 'w') as f:
    for word in sorted(words, key=lambda w: len(w), reverse=True):
        f.write(word)

我们可以找到最长的单词,而无需将完整的dict加载到内存中:

from collections import Counter
import sys


def check_same_letters(origin_map, word):
    new_map = Counter(word)
    return all(new_map[let] <= origin_map[let] for let in word)

def iter_longest_from_file(origin, file_path, min_length=1):
    origin_map = Counter(origin)
    origin_len = len(origin)
    with open(file_path) as f:
        for line in f:
            word = line.strip()
            if len(word) > origin_len:
                continue
            if len(word) < min_length:
                return
            if check_same_letters(origin_map, word):
                yield word

def find_longest_from_file(origin, file_path):
    return iter_longest_from_file(origin, file_path).next()

if __name__ == '__main__':
    origin = sys.argv[1] if len(sys.argv) > 1 else 'abcdefghijklmnopqrstuvwxyz'
    print find_longest_from_file(origin, 'words_by_len.txt')

答案 1 :(得分:4)

你想避免做排列。您可以计算字符在两个字符串中出现的次数(原始字符串和字典中的字符串)。从词典中删除字符频率不一样的所有单词。

因此,要检查字典中的一个单词,您需要在最多MAX(26,n)时间内计算字符数。

答案 2 :(得分:1)

  1. 将字典预解析为排序(单词),单词对。 (例如,giilnstu,语言学家)
  2. 对字典文件进行排序。
  3. 然后,当您搜索给定的一组字母时:

    1. 二进制搜索字典中的字母,首先对字母进行排序。
    2. 您需要为每个字长分别执行此操作。

      编辑:应该说您正在搜索目标字长(range(len(letters), 0, -1))的排序字母的所有唯一组合

答案 3 :(得分:1)

这类似于我之前研究过的一个问题。我通过使用素数来表示每个字母来解决这个问题。每个单词的字母乘积产生一个数字。要确定给定的一组输入字符是否足以完成工作,只需将输入字符的乘积除以您要检查的数字的乘积即可。如果没有余数,那么输入字符就足够了。我在下面实现了它。输出是:

$ python longest.py rasdaddea aosddna raepkwaen
rasdaddea -->  sadder
aosddna -->  soda
raepkwaen -->  reawaken

您可以在以下位置找到更多详细信息和对anagrams案例的详尽说明: http://mostlyhighperformance.blogspot.com/2012/01/generating-anagrams-efficient-and-easy.html

该算法花费少量时间来设置字典,然后对字典中的每个单词进行单独检查就像单个分区一样简单。可能有更快的方法依赖于关闭字典的部分,如果它没有字母,但如果你有大量的输入字母,这些可能最终表现更差,所以它实际上无法关闭字典的任何部分。

import sys


def nextprime(x):
    while True:
        x += 1
        for pot_fac in range(2,x):
            if x % pot_fac == 0:
                break
        else:
            return x

def prime_generator():
    '''Returns a generator that produces the next largest prime as
    compared to the one returned from this function the last time
    it was called. The first time it is called it will return 2.'''
    lastprime = 1
    while True:
        lastprime = nextprime(lastprime)
        yield lastprime


# Assign prime numbers to each lower case letter
gen = prime_generator()
primes = dict( [ (chr(x),gen.next()) for x in range(ord('a'),ord('z')+1) ] )


product = lambda x: reduce( lambda m,n: m*n, x, 1 )
make_key = lambda x: product( [ primes[y] for y in x ] )


try:
    words = open('words').readlines()
    words = [ ''.join( [ c for c in x.lower() \
                if ord('a') <= ord(c) <= ord('z') ] ) \
            for x in words ]
    for x in words:
        try:
            make_key(x)
        except:
            print x
            raise

except IOError:
    words = [ 'reawaken','awaken','enwrap','weaken','weaker', ]

words = dict( ( (make_key(x),x,) for x in words ) )


inputs = sys.argv[1:] if sys.argv[1:] else [ 'raepkwaen', ]
for input in inputs:
    input_key = make_key(input)
    results = [ words[x] for x in words if input_key % x == 0 ]

    result = reversed(sorted(results, key=len)).next()
    print input,'--> ',result

答案 4 :(得分:1)

我在问你这个问题后不久就开始了这个问题,但直到现在才开始对它进行抛光。这是我的解决方案,基本上是一个修改过的特里,我直到今天才知道!

class Node(object):
    __slots__ = ('words', 'letter', 'child', 'sib')

    def __init__(self, letter, sib=None):
        self.words = []
        self.letter = letter
        self.child = None
        self.sib = sib

    def get_child(self, letter, create=False):
        child = self.child
        if not child or child.letter > letter:
            if create:
                self.child = Node(letter, child)
                return self.child
            return None
        return child.get_sibling(letter, create)

    def get_sibling(self, letter, create=False):
        node = self
        while node:
            if node.letter == letter:
                return node
            sib = node.sib
            if not sib or sib.letter > letter:
                if create:
                    node.sib = Node(letter, sib)
                    node = node.sib
                    return node
                return None
            node = sib
        return None

    def __repr__(self):
        return '<Node({}){}{}: {}>'.format(chr(self.letter), 'C' if self.child else '', 'S' if self.sib else '', self.words)

def add_word(root, word):
    word = word.lower().strip()
    letters = [ord(c) for c in sorted(word)]
    node = root
    for letter in letters:
        node = node.get_child(letter, True)
    node.words.append(word)

def find_max_word(root, word):
    word = word.lower().strip()
    letters = [ord(c) for c in sorted(word)]
    words = []
    def grab_words(root, letters):
        last = None
        for idx, letter in enumerate(letters):
            if letter == last: # prevents duplication
                continue
            node = root.get_child(letter)
            if node:
                words.extend(node.words)
                grab_words(node, letters[idx+1:])
            last = letter
    grab_words(root, letters)
    return words

root = Node(0)
with open('/path/to/dict/file', 'rt') as f:
    for word in f:
        add_word(root, word)

测试:

>>> def nonrepeating_words():
...     return find_max_word(root, 'abcdefghijklmnopqrstuvwxyz')
... 
>>> sorted(nonrepeating_words(), key=len)[-10:]
['ambidextrously', 'troublemakings', 'dermatoglyphic', 'hydromagnetics', 'hydropneumatic', 'pyruvaldoxines', 'hyperabductions', 'uncopyrightable', 'dermatoglyphics', 'endolymphaticus']
>>> len(nonrepeating_words())
67590

我认为我更喜欢皮纹学,而不喜欢最长的单词,我自己。性能方面,使用~500k字词(来自here),

>>> import timeit
>>> timeit.timeit(nonrepeating_words, number=100)
62.8912091255188
>>> 

所以,平均而言,6/10秒(在我的i5-2500上)找到所有不包含重复字母的六万七千字。

这个实现与trie之间的巨大差异(这使得它甚至从一般的DAWG中进一步说明)是:单词存储在trie中与其排序的字母相关。所以'dog'这个词存放在与'god'相同的路径下:d-g-o。第二位是find_max_word算法,它确保通过不断削减其头部并重新运行搜索来访问每个可能的字母组合。

哦,只是为了咯咯笑:

>>> sorted(tree.find_max_word('RAEPKWAEN'), key=len)[-5:]
['wakener', 'rewaken', 'reawake', 'reawaken', 'awakener']

答案 5 :(得分:1)

与@ market的答案类似,另一种方法是为字典中的每个单词预先计算“位掩码”。如果字包含至少一个A,则设置位0,如果包含至少一个B,则设置位1,对于Z,依此类推至第25位。

如果要搜索字典中可以由字母组合组成的所有单词,首先要为字母集合形成位掩码。然后,您可以通过检查wordBitmask & ~lettersBitMask是否为零来过滤掉所有使用其他字母的单词。如果为零,则该单词仅使用集合中可用的字母,因此可能有效。如果这不是零,则使用集合中不可用的字母,因此不允许使用。

这种方法的优点是按位操作很快。字典中的绝大多数单词将使用17个或更多字母中的至少一个不在给定的集合中,并且您可以快速地将它们全部折扣。但是,对于通过过滤器的少数单词,还有一个检查,你仍然需要做。您仍然需要检查单词是否比使用集合中出现的字母更频繁地使用字母。例如,必须禁止使用'weakener'一词,因为它有三个'e',而在RAEPKWAEN字母集合中只有两个。单独的按位方法不会过滤掉这个单词,因为单词中的每个字母都出现在集合中。

答案 6 :(得分:0)

  1. 从词典中构建trie (prefix tree)。您可能想要缓存它。
  2. 走在这个特里,去掉那些不适合你的信件的整枝。
  3. 此时,你的特里是你字典中所有单词的代表,可以用你的字母包构成。

    1. 只需要更长的时间: - )
    2. 编辑:您也可以使用DAGW (Directed Acyclic Word Graph),它将拥有更少的顶点。虽然我还没有看过,但这篇维基百科文章有一个关于The World's Fastest Scrabble Program的链接。

答案 7 :(得分:0)

当查找长度超过10个字母的单词时,您可能会尝试迭代单词(我认为没有多少单词,包含10个字母)超过10个字母,并检查您的集合中是否有所需的字母。

问题是你必须首先找到所有len(word)&gt; = 10个单词。

那么,我会做什么: 阅读字典时,将单词分为两类:短片和长片。您可以通过迭代每个可能的排列来处理短路。通过迭代然后检查它们可以处理多头,这是可能的。

当然,两条路径都可以进行许多优化。

答案 8 :(得分:0)

DAWG(定向非循环字图) Mark Wutka非常友好地提供了一些pascal代码。

答案 9 :(得分:0)

如果您有包含已排序单词的文本文件。只需这段代码就可以了解数学:

UsrWrd = input()      #here you Enter scrambled letters
with open('words.db','r') as f:
   for Line in f:
       for Word in Line.split():
           if len(Word) == len(UsrWrd) and set(Word) == set(UsrWrd):
               print(Word)
               break
           else:continue       `