Эффективная охота за словами в зашифрованных буквах - PullRequest
9 голосов
/ 19 января 2012

Полагаю, вы могли бы классифицировать это как проблему в стиле скрэббл, но это началось из-за того, что один из друзей упомянул сериал британских телевизионных викторин 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 факториала = 362 880 и 8 факториала = 40 320. В этом масштабе, даже если бы пришлось проверять все возможные перестановки и длины слов, это не так много.

Однако, как только вы наберете 14 символов, это будет 87 178 291 200 возможных комбинаций, а это значит, что вы полагаетесь на удачу, что слово из 14 символов будет быстро найдено.

Из приведенного выше примера у моей машины уходит около 12 с половиной секунд, чтобы найти «пробуждение». Используя зашифрованные слова из 14 символов, мы могли бы разговаривать по шкале 23 дня, чтобы проверить все возможные варианты перестановок из 14 символов.

Есть ли более эффективный способ справиться с этим?

Ответы [ 10 ]

5 голосов
/ 19 января 2012

Реализация Jeroen Coupé идея от его ответ с количеством букв:

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 - может быть /usr/share/dict/words в Linux.

UPDATE

В случае, если нам нужно найти слово только один раз, и у нас есть словарь со словами, отсортированными по длине, например, по этому сценарию:

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)

Мы можем найти самое длинное слово без загрузки полного текста в память:

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')
4 голосов
/ 19 января 2012

Вы хотите избежать перестановок.Вы можете посчитать, сколько раз символ появляется в обеих строках (исходная строка и одна из словаря).Исключите все слова из словаря, где частота символов не одинакова.

Таким образом, чтобы проверить одно слово из словаря, вам нужно посчитать символы максимум в МАКС.

1 голос
/ 20 января 2012

Другой подход, аналогичный ответу @ market, заключается в предварительном вычислении «битовой маски» для каждого слова в словаре. Бит 0 устанавливается, если слово содержит хотя бы один A, бит 1 устанавливается, если он содержит хотя бы один B, и т. Д. До бита 25 для Z.

Если вы хотите найти все слова в словаре, которые могут состоять из комбинации букв, вы начинаете с формирования битовой маски для набора букв. Затем вы можете отфильтровать все слова, которые используют другие буквы, проверив, равен ли wordBitmask & ~lettersBitMask нулю. Если это ноль, слово использует только буквы, доступные в коллекции, и поэтому может быть допустимым. Если это ненулевое значение, используется буква, отсутствующая в коллекции, и поэтому не допускается.

Преимущество этого подхода в том, что побитовые операции выполняются быстро. Подавляющее большинство слов в словаре будет использовать по крайней мере одну из 17 или более букв, которых нет в данной коллекции, и вы можете быстро отменить их все. Тем не менее, для меньшего количества слов, которые проходят через фильтр, есть еще одна проверка, которую вам еще предстоит сделать. Вам все еще нужно убедиться, что слова не используют буквы чаще, чем они появляются в коллекции. Например, слово «слабее» должно быть запрещено, потому что у него три «е», тогда как в наборе букв RAEPKWAEN только два. Один только побитовый подход не отфильтрует это слово, поскольку каждая буква в слове появляется в коллекции.

1 голос
/ 20 января 2012

Я начал это прошлой ночью вскоре после того, как вы задали вопрос, но до сих пор не полировал его до полировки.Это было мое решение, которое по сути представляет собой модифицированный три, о котором я не знал до сегодняшнего дня!

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

Я думаю, что я предпочитаю дерматоглифику неконкурентоспособной дольшеслово, яС точки зрения производительности, используя словарь ~ 500 000 слов (из здесь ),

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

Таким образом, в среднем, 6/10 секунды (на моем i5-2500), чтобы найтивсе шестьдесят семь тысяч слов, которые не содержат повторяющихся букв.

Большие различия между этой реализацией и деревом (что делает его еще более далеко от DAWG в целом) состоит в том, что: слова хранятся в дереве в отношениина их отсортированные письма.Таким образом, слово «собака» хранится по тому же пути, что и «бог»: dgo.Второй бит - это алгоритм find_max_word, который обеспечивает посещение каждой возможной комбинации букв, постоянно отрывая голову и повторяя поиск.

О, и только для хихиканья:

>>> sorted(tree.find_max_word('RAEPKWAEN'), key=len)[-5:]
['wakener', 'rewaken', 'reawake', 'reawaken', 'awakener']
1 голос
/ 19 января 2012

Это похоже на проблему с анаграммой, над которой я работал раньше.Я решил это, используя простые числа для представления каждой буквы.Произведение букв для каждого слова производит число.Чтобы определить, достаточен ли данный набор входных символов для выполнения работы, просто разделите произведение входного символа на произведение для числа, которое вы хотите проверить.Если остатка нет, тогда достаточно ввести символы.Я реализовал это ниже.Вывод:

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

Более подробную информацию и подробное объяснение случая анаграмм можно найти по адресу: 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
1 голос
/ 19 января 2012
  1. Предварительно проанализировать словарь как отсортированный (слово), пары слов. (например, Гилнсту, лингвист)
  2. Сортировать файл словаря.

Затем, когда вы ищете заданный набор букв:

  1. Двоичный поиск в словаре по имеющимся буквам, сначала сортировка букв.

Вам нужно будет сделать это отдельно для каждой длины слова.

РЕДАКТИРОВАТЬ: следует сказать, что вы ищете все уникальные комбинации отсортированных букв целевой длины слова (range(len(letters), 0, -1))

0 голосов
/ 29 апреля 2018

Если у вас есть текстовый файл с отсортированными словами.Просто этот код делает математику:

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       `
0 голосов
/ 20 января 2012

DAWG (Направленный ациклический словесный граф) Марк Ватка любезно предоставил здесь некоторый паскаль-код.

0 голосов
/ 19 января 2012

При поиске слов длиннее 10 букв вы можете попытаться перебрать слова (я думаю, что не так много слов с 10 буквами) длиннее 10 букв, и проверьте, что в вашем наборе есть требуемые буквы.

Проблема в том, что вы должны сначала найти все эти слова (слова)> = 10 слов.

Итак, что бы я сделал: При чтении словаря разделите слова на 2 категории: короткие и длинные,Вы можете обрабатывать шорты, повторяя каждую возможную перестановку.Чем вы можете обрабатывать long, повторяя их и проверяя, что они возможны.

Конечно, существует много возможностей оптимизации для обоих путей.

0 голосов
/ 19 января 2012
  1. Создайте три (дерево префиксов) из вашего словаря.Возможно, вы захотите его кешировать.
  2. Пройдите по этому дереву и удалите целые ветви, которые не помещаются в ваш пакет с буквами.

На этом этапе ваше дерево представляет всеслова в вашем словаре, которые можно составить из вашей сумки букв.

  1. Просто возьмите более длинные слова: -)

Редактировать: вы также можете использовать DAGW (направленный график ациклических слов) , который будет иметь меньше вершин.Хотя я не читал ее, в этой статье в Википедии есть ссылка о самой быстрой в мире программе скрэббл .

...