web-dev-qa-db-fra.com

Filtrer un ensemble pour faire correspondre les permutations de chaînes

J'essaie d'utiliser itertools.permutations () pour renvoyer toutes les permutations de la chaîne et ne renvoyer que celles qui sont membres d'un ensemble de mots.

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']
    '''

Ma solution actuelle fonctionne très bien dans le terminal mais n'a pas réussi à passer le test ...

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

Toute aide serait appréciée.

16
Meruemu

Vous pouvez simplement utiliser collections.Counter() pour comparer le words au string sans créer tout permutations (cela explose avec la longueur de la chaîne):

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']

Remarque: sets ne sont pas classés, donc si vous avez besoin d'une commande spécifique, vous devrez peut-être trier le résultat, par exemple return sorted(...)

12
AChampion

Catégorie de problème

Le problème que vous résolvez est mieux décrit comme un test pour les correspondances anagram .

Solution utilisant Trier

solution traditionnelle consiste à trier la chaîne cible, à trier la chaîne candidate et à tester l'égalité.

>>> 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']

Solution utilisant des multi-ensembles

Une autre approche consiste à utiliser collections.Counter () pour effectuer un test d'égalité multiset . Ceci est algorithmiquement supérieur à la solution de tri (O(n) contre O(n log n)) mais a tendance à perdre à moins que la taille des chaînes soit grande (en raison du coût de hachage de tous les caractères).

>>> 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']

Solution utilisant un hachage parfait

Une signature d'anagramme unique ou hachage parfait peut être construite en multipliant les nombres premiers correspondant à chaque caractère possible dans une chaîne.

La propriété commutative de multiplication garantit que la valeur de hachage sera invariante pour toute permutation d'une seule chaîne. L'unicité de la valeur de hachage est garantie par le théorème fondamental de l'arithmétique (également connu sous le nom de théorème de factorisation premier unique).

>>> 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']

Solution utilisant des permutations

La recherche par permutations sur la chaîne cible en utilisant itertools.permutations () est raisonnable lorsque la chaîne est petite (générer des permutations sur a n = la chaîne de longueur génère n candidats factoriels).

La bonne nouvelle est que lorsque n est petit et que le nombre de mots est grand, cette approche fonctionne très rapidement (car le test d'appartenance à l'ensemble est 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']

Comme l'OP l'a supposé, la boucle de recherche pure python peut être accélérée à la vitesse c en utilisant 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']

Meilleure solution

La meilleure solution dépend de la longueur de chaîne et de la longueur de mots. Les horaires indiqueront ce qui convient le mieux à un problème particulier.

Voici quelques timings comparatifs pour les différentes approches utilisant deux tailles de chaîne différentes:

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

Les résultats indiquent que pour les petites chaînes, l'approche la plus rapide recherche les permutations sur la chaîne cible en utilisant set-intersection.

Pour les chaînes plus grandes, l'approche la plus rapide est la solution traditionnelle de tri et de comparaison.

J'espère que vous avez trouvé cette petite étude algorithmique aussi intéressante que moi. Les plats à emporter sont:

  • Les ensembles, les outils itératifs et les collections permettent de résoudre rapidement de tels problèmes.
  • Les temps de fonctionnement Big Oh comptent (le facteur n se désintègre pour les grands n).
  • Les frais généraux constants sont importants (le tri bat les multisets à cause des frais généraux de hachage).
  • Les mathématiques discrètes sont un trésor d'idées.
  • Il est difficile de savoir ce qu'il y a de mieux tant que vous n'avez pas analysé et exécuté les horaires :-)

Configuration du calendrier

FWIW, voici une configuration de test que j'ai utilisée pour exécuter les timings comparatifs:

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__)
113
Raymond Hettinger

Apparemment, vous vous attendez à ce que la sortie soit triée par ordre alphabétique, donc cela devrait faire:

return sorted(set(''.join(p) for p in itertools.permutations(string)) & words)
3
Błotosmętek

Essayez cette solution

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

On peut l'appeler listA

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

Votre liste est ListB

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

Utilisez ensuite l'intersection définie

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