Поиск комбинаций стеблей и концов

У меня есть отображения "стеблей" и "окончаний" (могут быть не правильные слова), которые выглядят так:

all_endings = {
 'birth': set(['place', 'day', 'mark']), 
 'snow': set(['plow', 'storm', 'flake', 'man']),
 'shoe': set(['lace', 'string', 'maker']),
 'lock': set(['down', 'up', 'smith']),
 'crack': set(['down', 'up',]),
 'arm': set(['chair']),
 'high': set(['chair']),
 'over': set(['charge']),
 'under': set(['charge']),
}

Но гораздо дольше, конечно. Я также сделал соответствующий словарь по-другому:

all_stems = {
 'chair': set(['high', 'arm']),
 'charge': set(['over', 'under']),
 'up': set(['lock', 'crack', 'vote']),
 'down': set(['lock', 'crack', 'fall']),
 'smith': set(['lock']),
 'place': set(['birth']),
 'day': set(['birth']),
 'mark': set(['birth']),
 'plow': set(['snow']),
 'storm': set(['snow']),
 'flake': set(['snow']),
 'man': set(['snow']),
 'lace': set(['shoe']),
 'string': set(['shoe']),
 'maker': set(['shoe']),
}

Теперь я попытался найти алгоритм, чтобы найти совпадение двух или более "стеблей", которые соответствуют двум или более "концам". Выше, например, он будет соответствовать вниз и вверх с блокировкой и трещиной, в результате чего

lockdown
lockup
crackdown
crackup

Но не включая 'upvote', 'downfall' or 'locksmith' (и это то, что вызывает у меня самые большие проблемы). Я получаю ложные срабатывания вроде:

pancake
cupcake
cupboard

Но я просто обойдусь "петлями". (Пун предназначался), и я, кажется, никуда не денутся. Я был бы признателен за любой удар в правильном направлении.

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

findings = defaultdict(set)
for stem, endings in all_endings.items():
    # What stems have matching endings:
    for ending in endings:
        otherstems = all_stems[ending]
        if not otherstems:
            continue
        for otherstem in otherstems:
            # Find endings that also exist for other stems
            otherendings = all_endings[otherstem].intersection(endings)
            if otherendings:
                # Some kind of match
                findings[stem].add(otherstem)

# Go through this in order of what is the most stems that match:

MINMATCH = 2
for match in sorted(findings.values(), key=len, reverse=True):
    for this_stem in match:
        other_stems = set() # Stems that have endings in common with this_stem
        other_endings = set() # Endings this stem have in common with other stems
        this_endings = all_endings[this_stem]
        for this_ending in this_endings:
            for other_stem in all_stems[this_ending] - set([this_stem]):
                matching_endings = this_endings.intersection(all_endings[other_stem])
                if matching_endings:
                    other_endings.add(this_ending)
                    other_stems.add(other_stem)

        stem_matches = all_stems[other_endings.pop()]
        for other in other_endings:
            stem_matches = stem_matches.intersection(all_stems[other])

        if len(stem_matches) >= MINMATCH:
            for m in stem_matches:
                for e in all_endings[m]:
                    print(m+e)

Ответ 1

Это не особенно красиво, но это довольно просто, если вы разложите словарь на два списка и используете явные индексы:

all_stems = {
 'chair' : set(['high', 'arm']),
 'charge': set(['over', 'under']),
 'fall'  : set(['down', 'water', 'night']),
 'up'    : set(['lock', 'crack', 'vote']),
 'down'  : set(['lock', 'crack', 'fall']),
}

endings     = all_stems.keys()
stem_sets   = all_stems.values()

i = 0
for target_stem_set in stem_sets:
    i += 1
    j  = 0

    remaining_stems = stem_sets[i:]
    for remaining_stem_set in remaining_stems:
        j += 1
        union = target_stem_set & remaining_stem_set
        if len(union) > 1:
            print "%d matches found" % len(union)
            for stem in union:
                print "%s%s" % (stem, endings[i-1])
                print "%s%s" % (stem, endings[j+i-1])

Вывод:

$ python stems_and_endings.py 
2 matches found
lockdown
lockup
crackdown
crackup

В основном все, что мы делаем, это итерация через каждый набор по очереди и сравнение его с каждым оставшимся набором, чтобы увидеть, есть ли более двух совпадений. Нам никогда не приходится пытаться выполнять наборы, которые падают раньше текущего набора, потому что они уже были сопоставлены в предыдущей итерации. Остальное (индексирование и т.д.) - это просто бухгалтерский учет.

Ответ 2

Я думаю, что способ избежать этих ложных срабатываний - удалить кандидатов без слов в пересечении стеблей. Если это имеет смысл:(

Пожалуйста, посмотрите и, пожалуйста, дайте мне знать, если я что-то упустил.

#using all_stems and all_endings from the question

#this function is declared at the end of this answer
two_or_more_stem_combinations = get_stem_combinations(all_stems)
print "two_or_more_stem_combinations", two_or_more_stem_combinations
#this print shows ... [set(['lock', 'crack'])] 

for request in two_or_more_stem_combinations:
    #we filter the initial index to only look for sets or words in the request
    candidates = filter(lambda x: x[0] in request, all_endings.items())

    #intersection of the words for the request
    words = candidates[0][1]
    for c in  candidates[1:]:
        words=words.intersection(c[1])

    #it handy to have it in a dict
    candidates = dict(candidates)

    #we need to remove those that do not contain 
    #any words after the intersection of stems of all the candidates
    candidates_to_remove = set()
    for c in candidates.items():
        if len(c[1].intersection(words)) == 0:
        candidates_to_remove.add(c[0])

    for key in candidates_to_remove:
        del candidates[key]

    #now we know what to combine
    for c in candidates.keys():
       print "combine", c , "with", words 

Выход:

объединить блокировку с множеством (['down', 'up'])

объединить трещины с множеством (['down', 'up'])

Как вы можете видеть, это решение не содержит эти ложные срабатывания.

Изменить: сложность

И сложность этого решения не хуже, чем O (3n) в худшем сценарии - без учета доступа к словарям. А также для большинства исполнений первый фильтр сужает довольно много пространства решений.

Изменить: получение стеблей

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

def get_stems_recursive(stems,partial,result,at_least=2):
    if len(partial) >= at_least:
        stem_intersect=all_stems[partial[0]]
        for x in partial[1:]:
           stem_intersect = stem_intersect.intersection(all_stems[x])
           if len(stem_intersect) < 2:
               return
        result.append(stem_intersect)

    for i in range(len(stems)):
        remaining = stems[i+1:]
        get_stems_recursive(remaining,partial + [stems[i][0]],result)


def get_stem_combinations(all_stems,at_least=2):
    result = []
    get_stems_recursive(all_stems.items(),list(),result)
    return result

two_or_more_stem_combinations = get_stem_combinations(all_stems)

Ответ 3

== Отредактированный ответ: ==

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

from itertools import combinations

MINMATCH = 2
print 'all words with at least', MINMATCH, 'endings in common:'
for (word0,word1) in combinations(stems_by_endings, 2):
    ending_words0 = stems_by_endings[word0]
    ending_words1 = stems_by_endings[word1]
    common_endings = ending_words0 & ending_words1
    if len(common_endings) >= MINMATCH:
        for stem in common_endings:
            print ' ', stem+word0
            print ' ', stem+word1

# all words with at least 2 endings in common:
#   lockdown
#   lockup
#   falldown
#   fallup
#   crackdown
#   crackup

== Предыдущий ответ ==

Я не пытаюсь много оптимизировать, но здесь несколько грубой силы, но короткого подхода, который сначала вычисляет "end_sets" для каждого слова стебля, а затем находит все слова стебля, которые имеют общие конечные_секунды, по крайней мере, указанное минимальное количество общих окончаний.

В заключительной фазе он печатает все возможные комбинации этих слов стебля + окончания, которые он обнаружил, которые соответствуют критериям. Я попытался сделать все имена переменных максимально описательными, чтобы было легко следовать.;-) Я также не учитывал определения all_endings' and 'all+stems.

from collections import defaultdict
from itertools import combinations

ending_sets = defaultdict(set)
for stem in all_stems:
    # create a set of all endings that have this as stem
    for ending in all_endings:
        if stem in all_endings[ending]:
            ending_sets[stem].add(ending)


MINMATCH = 2
print 'all words with at least', MINMATCH, 'endings in common:'
for (word0,word1) in combinations(ending_sets, 2):
    ending_words0 = ending_sets[word0]
    ending_words1 = ending_sets[word1]
    if len(ending_words0) >= MINMATCH and ending_words0 == ending_words1:
        for stem in ending_words0:
            print ' ', stem+word0
            print ' ', stem+word1

# output
# all words with at least 2 endings in common:
#   lockup
#   lockdown
#   crackup
#   crackdown

Ответ 4

Если вы представляете свои связи в квадратных двоичных массивах (где 1 означает, что "x может следовать за y", например, и где другие элементы установлены в 0), то, что вы пытаетесь сделать, эквивалентно поиску "сломанные прямоугольники", заполненные единицами:

       ... lock  **0  crack  **1  ...

...    ...
down   ...  1     0     1     1
up     ...  1     1     1     1
...    ...

Здесь lock, crack и **1 (примерное слово) можно сопоставить с down и up (но не с словом **0). Сопряженные отношения рисуют прямоугольник 2x3, заполненный с помощью.

Надеюсь, это поможет!