Эффективный способ найти анаграммы предложений из словаря?

Мне нужно сделать программу, которая берет на вход файл со словарем и произвольную строку, а затем выводит все комбинации слов из этого словаря, составляющие анаграммы заданной строки. Например, используя 100 самых популярных слов английского языка и строку "i not work", я должен получить что-то вроде [' on it work', ' into work', ' not i work', ' know or it', ' work it no', ' to work in'], что я и делаю.

Проблема в том, что моя программа слишком неэффективна: при 100 словах в словаре практический предел составляет 7 символов для длины строки, все после этого занимает слишком много времени. Я пытался искать различные алгоритмы, связанные с этим вопросом, но безрезультатно.

Вот как я ищу анаграммы:

def sortstring(string):
    return ''.join(sorted(string))

def simplify(all_strings):
    possible_strings = defaultdict(list)
    for string in all_strings:
        possible_strings[sortstring(string).strip()].append(string)
    return possible_strings

def generate(database, length,curstring="", curdata=set()):
    if len(curstring.replace(" ", "")) > length:
        return set()
    if len((curstring).replace(" ", "")) == length:
        return curdata.union(set([curstring]))
    for i in database:
        if len((curstring+i).replace(" ", "")) <= length:
            curdata = curdata.union(generate(database.difference(set([i])),
                length, curstring+" "+i, curdata))
            database = database.difference(set([i]))
    return curdata

def analyse(database, input_string):
    cletters = countstring(input_string)
    strings = simplify(generate(database, cletters))
    data = list()
    sorted_string = sortstring(input_string).strip()
    if sorted_string in strings.keys():
        data = strings[sorted_string]
    return len(strings.values()), data

def countstring(string):
    a = countletters(string)
    return sum(a.values())

def countletters(string):
    result = {}
    for i in ascii_lowercase:
        result[i] = string.count(i)
    return result

Может ли кто-нибудь предложить способ улучшить это? Хотя я полагаю, что алгоритм, который я использовал, должен быть полностью исключен, учитывая, что сложность кажется слишком высокой из-за того, насколько он медленный. На всякий случай: программа должна быть достаточно производительной, чтобы поддерживать словари из десятков тысяч слов и строк до десятков символов. Это намного лучше, чем то, что я сделал.


person Mashallah    schedule 11.12.2015    source источник
comment
Несколько тестов: для 2-буквенной строки время генерации составляет 0,00085 с, для 3-буквенной строки время генерации составляет 0,0039 с, для 4-буквенной строки время генерации составляет 0,018 с, для 5-буквенной строки время генерации составляет 0,0039 с. буквенная строка, время генерации 0,05 с, для 6-буквенной строки время генерации 0,48 с, для 7-буквенной строки время генерации 4,2 с.   -  person Mashallah    schedule 11.12.2015
comment
Другими словами, каждая дополнительная буква увеличивает время выполнения примерно в 3-10 раз. И это со словарем на 100 слов   -  person Mashallah    schedule 11.12.2015
comment
Сначала напишите функцию, которая идентифицирует все слова, которые могут быть образованы из букв строки, а затем используйте эту функцию в алгоритме поиска с возвратом.   -  person John Coleman    schedule 11.12.2015
comment
Первая часть проста, но как мне вернуться назад?   -  person Mashallah    schedule 11.12.2015
comment
Что-то вроде обхода дерева. Пустая строка является корнем. Слова в словаре, которые можно составить из букв, являются детскими. Когда вы посещаете узел, дочерними элементами этого узла являются слова, которые можно составить из оставшихся букв. Любое такое слово также появляется в списках возможных слов на один уровень выше, поэтому вы сможете очень быстро определить, какие слова все еще возможны. Если вы доберетесь до узла, где есть оставшиеся буквы, которые не могут быть сформированы ни в одно слово — вернитесь назад. Если вы дойдете до узла, в котором не осталось букв, путь от корня к этому узлу будет одной из искомых вами анаграмм.   -  person John Coleman    schedule 11.12.2015
comment
Какова будет сложность этого алгоритма? Судя по звуку, мне пришлось бы восстанавливать список возможных слов на каждом шагу, что заняло бы много времени с длинным словарем, так как мне приходилось бы каждый раз проходить его заново. Или я вас неправильно понял?   -  person Mashallah    schedule 11.12.2015
comment
Вам не нужно регенерировать список с нуля на каждом этапе — по мере продвижения вниз по дереву вы отбрасываете возможные слова, а не добавляете новые. Весь словарь нужно обработать только один раз.   -  person John Coleman    schedule 11.12.2015
comment
Кажется, я понимаю, что вы имеете в виду. Я попробую реализовать этот алгоритм (оставив комментарий к исходному алгоритму) и сравнив скорость вычислений.   -  person Mashallah    schedule 11.12.2015
comment
Я реализовал это, и это привело лишь к небольшой оптимизации. Используя словарь из 58000 слов и строку there is none для теста, программе требуется около 37 секунд, чтобы найти все анаграммы.   -  person Mashallah    schedule 11.12.2015
comment
Я украсил ваш код. Недостаток пробелов резал мои сверхчувствительные глаза.   -  person Zenadix    schedule 11.12.2015


Ответы (2)


Часть проблемы решил сам. Устранен антипаттерн for-if в коде генератора:

def generate(database, length,letters,curstring="",curdata=set()):
if len(curstring.replace(" ",""))>length:
    return set()
if len((curstring).replace(" ",""))==length:
    return curdata.union(set([curstring]))
t=countletters(curstring)
for i in ascii_lowercase:
    if t[i]>letters[i]:
        return set()
for i in database:
    t=countletters(curstring+i)
    test=0
    for j in ascii_lowercase:
        if t[j]>letters[j]:
            test=1
    if test: continue
    if sum(t.values())<=length:
        curdata=curdata.union(generate(database.difference(set([i])),length,letters,curstring+" "+i,curdata))
        database=database.difference(set([i]))
return curdata

Теперь это намного, намного быстрее, но все еще медленно, если словарь содержит десятки тысяч слов и/или если входная строка длинная.

person Mashallah    schedule 11.12.2015

Вот рекурсивный подход, реализующий древовидный подход, который я предложил в комментариях:

def frequencyDict(s):
    s = s.lower()
    d = {}
    for c in s:
        if c.isalpha():
            if c in d:
                d[c] += 1
            else:
                d[c] = 1
    return d

def canMake(w,fdict):
    d = frequencyDict(w)
    return all(d[c] <= fdict.get(c,0) for c in d)

def candidates(wlist,fdict):
    return [w for w in wlist if canMake(w,fdict)]

def anagrams(wlist,fdict):
    if len(wlist) == 0 or len(fdict) == 0:
        return "no anagrams"
    hits = []
    firstWords = candidates(wlist,fdict)
    if len(firstWords) == 0:
        return "no anagrams"
    for w in firstWords:
        #create reduced frequency dict
        d = fdict.copy() 
        for c in w:
            d[c] -= 1
            if d[c] == 0: del d[c]
        #if d is empty, the first word is also a the last word
        if len(d) == 0:
            hits.append(w)
        else:
            #create reduced word list
            rlist = [v for v in wlist if canMake(v,d)]
            tails = anagrams(rlist, d)
            if tails != "no anagrams":
                hits.extend(w + " " + t for t in tails)
    if len(hits) == 0:
        return "no anagrams"
    else:
        return hits

def findAnagrams(wlist,s):
    return anagrams(wlist,frequencyDict(s.lower()))

f = open("linuxwords.txt")
words = f.read().split('\n')
f.close()
words = [w.strip().lower() for w in words if not '-' in w]
test = findAnagrams(words, "Donald Trump")

Требуется около 20 секунд, чтобы найти все 730 анаграмм «Дональд Трамп», взятых из старого списка слов Linux. Мой любимый "Владыка сырого ореха"

person John Coleman    schedule 11.12.2015
comment
Это кажется медленнее, чем моя текущая реализация. В случае 2645 анаграмм программе потребовалось около 3 секунд, чтобы найти все анаграммы. - person Mashallah; 11.12.2015
comment
@Mashallah Я уверен, что есть оптимизации. Кроме того, мой код находит все упорядоченные анаграммы (владыка влажного ореха, влажный лорд влажного ореха и т. д. для всех 3! = 6) перестановок. Оптимизация заключалась бы в написании функции, которая возвращает мультимножества, такие как {'damp','nut','lord'}, а не строки. - person John Coleman; 11.12.2015