Фильтрация набора для соответствия строковых перестановок

Я пытаюсь использовать itertools.permutations(), чтобы возвращать все перестановки строки и возвращать только те, которые являются членами набора слов.

import itertools

def permutations_in_dict(string, words): 
    '''
    Parameters
    ----------
    string : {str}
    words : {set}

    Returns
    -------
    list : {list} of {str}    

    Example
    -------
    >>> permutations_in_dict('act', {'cat', 'rat', 'dog', 'act'})
    ['act', 'cat']
    '''

Мое текущее решение отлично работает в терминале, но как-то не могло пройти тестовый сценарий...

return list(set([''.join(p) for p in itertools.permutations(string)]) & words)

Любая помощь будет оценена.

Ответы

Ответ 1

Вы можете просто использовать collections.Counter() для сравнения words с string без создания всего permutations (это взрывается с длиной строки):

from collections import Counter

def permutations_in_dict(string, words):
    c = Counter(string)
    return [w for w in words if c == Counter(w)]

>>> permutations_in_dict('act', {'cat', 'rat', 'dog', 'act'})
['cat', 'act']

Примечание: set являются неупорядоченными, поэтому, если вам нужен конкретный заказ, вам может понадобиться сортировать результат, например. return sorted(...)

Ответ 2

Проблема Категория

Задача, которую вы решаете, лучше всего описать как тестирование для anagram.

Решение с помощью Sort

Традиционным решением является сортировка целевой строки, сортировка строки-кандидата и проверка равенства.

>>> def permutations_in_dict(string, words):
        target = sorted(string)
        return sorted(word for word in words if sorted(word) == target)

>>> permutations_in_dict('act', {'cat', 'rat', 'dog', 'act'})
['act', 'cat']

Решение с использованием Multisets

Другой подход - использовать collections.Counter(), чтобы сделать multiset тест равенства. Это алгоритмически превосходит решение сортировки (O(n) по сравнению с O(n log n)), но имеет тенденцию терять, если размер строк невелик (из-за стоимости хэширования всех символов).

>>> def permutations_in_dict(string, words):
        target = Counter(string)
        return sorted(word for word in words if Counter(word) == target)

>>> permutations_in_dict('act', {'cat', 'rat', 'dog', 'act'})
['act', 'cat']

Решение с использованием Perfect Hash

Уникальная сигнатура анаграммы или perfect hash может быть построена путем умножения простых чисел, соответствующих каждому возможному символу в строке.

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

>>> from operator import mul
>>> primes = [2, 3, 5, 7, 11]
>>> primes += [p for p in range(13, 1620) if all(pow(b, p-1, p) == 1 for b in (5, 11))]
>>> anagram_hash = lambda s: reduce(mul, (primes[ord(c)] for c in s))
>>> def permutations_in_dict(string, words):
        target = anagram_hash(string)
        return sorted(word for word in words if anagram_hash(word) == target)

>>> permutations_in_dict('act', {'cat', 'rat', 'dog', 'act'})
['act', 'cat']

Решение с использованием перестановок

Поиск по перестановкам в целевой строке с использованием itertools.permutations() разумно, когда строка мала (генерирование перестановок на строке длины генерирует n факториальных кандидатов).

Хорошей новостью является то, что, когда n мало и количество слов велико, этот подход выполняется очень быстро (поскольку тестирование набора членства является O (1)):

>>> from itertools import permutations
>>> def permutations_in_dict(string, words):
        perms = set(map(''.join, permutations(string)))
        return sorted(word for word in words if word in perms)

>>> permutations_in_dict('act', {'cat', 'rat', 'dog', 'act'})
['act', 'cat']

Как предполагалось в OP, чистый цикл поиска python может быть ускорен до c-скорости с помощью set.intersection():

>>> def permutations_in_dict(string, words):
        perms = set(map(''.join, permutations(string)))
        return sorted(words & perms)

>>> permutations_in_dict('act', {'cat', 'rat', 'dog', 'act'})
['act', 'cat']

Лучшее решение

Какое решение лучше всего зависит от длины строки и длины слов. Сроки показывают, что лучше всего подходит для конкретной проблемы.

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

Timings with string_size=5 and words_size=1000000
-------------------------------------------------
0.01406    match_sort
0.06827    match_multiset
0.02167    match_perfect_hash
0.00224    match_permutations
0.00013    match_permutations_set

Timings with string_size=20 and words_size=1000000
--------------------------------------------------
2.19771    match_sort
8.38644    match_multiset
4.22723    match_perfect_hash
<takes "forever"> match_permutations
<takes "forever"> match_permutations_set

Результаты показывают, что для небольших строк самый быстрый поиск ищет перестановки в целевой строке с использованием set-intersection.

Для больших строк самый быстрый подход - это традиционное решение для сортировки и сравнения.

Надеюсь, вы нашли это небольшое алгоритмическое исследование таким же интересным, как я. Приемы:

  • Установки, itertools и коллекции делают короткую работу таких проблем.
  • Большее-ое время работы материя (n-факториал распадается при больших n).
  • Постоянные служебные вопросы (сортировка удаляет мультимножества из-за хеширования).
  • Дискретная математика - это сокровищница идей.
  • Трудно узнать, что лучше, пока вы не проведете анализ и не запустите тайминги: -)

Настройка времени

FWIW, вот тестовая настройка, которую я использовал для запуска сравнительных таймингов:

from collections import Counter
from itertools import permutations
from string import letters
from random import choice
from operator import mul
from time import time

def match_sort(string, words):
    target = sorted(string)
    return sorted(word for word in words if sorted(word) == target)

def match_multiset(string, words):
    target = Counter(string)
    return sorted(word for word in words if Counter(word) == target)

primes = [2, 3, 5, 7, 11]
primes += [p for p in range(13, 1620) if all(pow(b, p-1, p) == 1 for b in (5, 11))]
anagram_hash = lambda s: reduce(mul, (primes[ord(c)] for c in s))

def match_perfect_hash(string, words):
    target = anagram_hash(string)
    return sorted(word for word in words if anagram_hash(word) == target)

def match_permutations(string, words):
    perms = set(map(''.join, permutations(string)))
    return sorted(word for word in words if word in perms)

def match_permutations_set(string, words):
    perms = set(map(''.join, permutations(string)))
    return sorted(words & perms)

string_size = 5
words_size = 1000000

population = letters[: string_size+2]
words = set()
for i in range(words_size):
    word = ''.join([choice(population) for i in range(string_size)])
    words.add(word)
string = word                # Arbitrarily search use the last word as the target

print 'Timings with string_size=%d and words_size=%d' % (string_size, words_size)
for func in (match_sort, match_multiset, match_perfect_hash, match_permutations, match_permutations_set):
    start = time()
    func(string, words)
    end = time()
    print '%-10.5f %s' % (end - start, func.__name__)

Ответ 3

Попробуйте это решение

list(map("".join, itertools.permutations('act')))
['act', 'atc', 'cat', 'cta', 'tac', 'tca']

Мы можем назвать его listA

listA = list(map("".join, itertools.permutations('act')))

Ваш список - ListB

listB = ['cat', 'rat', 'dog', 'act']

Затем используйте пересечение множеств

list(set(listA) & set(listB))
['cat', 'act']

Ответ 4

По-видимому, вы ожидаете, что вывод будет отсортирован в алфавитном порядке, поэтому это должно сделать:

return sorted(set(''.join(p) for p in itertools.permutations(string)) & words)

Ответ 5

Совпадение наборов букв

set_string = set(string)    
return [w for w in words if set(w) == set_string]

Ниже приведены тайминги из верхнего ответа (Python 3.6)

0.06000    match_multiset
0.02201    match_perfect_hash
0.00900    match_sort
0.00600    comprehension  <-- This answer
0.00098    match_permutations
0.00000    match_permutations_set

Ответ 6

Зачем даже перестановки? Это гораздо более простая проблема, если вы посмотрите на слова как словари букв. Я уверен, что есть понимание, чтобы сделать это лучше, чем это, но:

    letters = dict()
    for i in word:
      letters[i] = letters.get(i, 0) + 1

сделайте это для слова then для каждого слова в наборе, убедитесь, что значение для каждой клавиши больше или равно значению этого слова. Если это так, добавьте его в свой вывод.

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