Фильтрация набора для соответствия строковых перестановок
Я пытаюсь использовать 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 для каждого слова в наборе, убедитесь, что значение для каждой клавиши больше или равно значению этого слова. Если это так, добавьте его в свой вывод.
Добавленный бонус: это должно быть легко распараллеливать, если ваш список слов чрезвычайно длинный.