web-dev-qa-db-fra.com

Existe-t-il de meilleures méthodes pour permuter la chaîne?

void permute(string elems, int mid, int end)
{
    static int count;
    if (mid == end) {
        cout << ++count << " : " << elems << endl;
        return ;
    }
    else {
    for (int i = mid; i <= end; i++) {
            swap(elems, mid, i);
            permute(elems, mid + 1, end);
            swap(elems, mid, i);
        }
    }
}

La fonction ci-dessus montre les permutations de str (avec str[0..mid-1] comme préfixe permanent et str[mid..end] comme suffixe permutable). Nous pouvons donc utiliser permute(str, 0, str.size() - 1) pour afficher toutes les permutations d’une chaîne.

Mais la fonction utilise un algorithme récursif; peut-être que ses performances pourraient être améliorées?

Existe-t-il de meilleures méthodes pour permuter une chaîne?

50
Jichao

Voici un algorithme non récursif en C++ à partir de l'entrée Wikipedia pour génération non ordonnée de permutations . Pour la chaîne s de longueur n, pour toute k de 0 à n! - 1 inclus, ce qui suit modifie s pour fournir une permutation unique (différente de celles générées pour toute autre valeur k sur cette plage). Pour générer toutes les permutations, exécutez-le pour tout n! k valeurs sur la valeur d'origine de s.

#include <algorithm>

void permutation(int k, string &s) 
{
    for(int j = 1; j < s.size(); ++j) 
    {
        std::swap(s[k % (j + 1)], s[j]); 
        k = k / (j + 1);
    }
}

Ici swap(s, i, j) permute les positions i et j de la chaîne s. 

63
Permaquid

Pourquoi n'essayez-vous pas std::next_permutation() ou std::prev_permutation()?

Liens:

std :: next_permutation ()
std :: prev_permutation ()

Un exemple simple:

#include<string>
#include<iostream>
#include<algorithm>

int main()
{
   std::string s="123";
   do
   {

      std::cout<<s<<std::endl;

   }while(std::next_permutation(s.begin(),s.end()));
}

Sortie:

123
132
213
231
312
321
48
Prasoon Saurav

Je voudrais seconder réponse de Permaquid . L'algorithme qu'il cite fonctionne d'une manière fondamentalement différente des divers algorithmes d'énumération par permutation proposés. Il ne génère pas toutes les permutations de n objets, il génère une permutation spécifique distincte, étant donné un entier compris entre 0 and n!-1. Si vous n'avez besoin que d'une permutation spécifique, c'est beaucoup plus rapide que de les énumérer toutes puis de les sélectionner. 

Même si vous avez besoin de toutes les permutations, il fournit des options qu'un algorithme de dénombrement par permutation unique ne permet pas. Une fois, j’ai écrit un cracker cryptarithmique à force brute, qui essayait toutes les assignations possibles de lettres en chiffres. Pour les problèmes de base-10, c'était suffisant, car il n'y a que __ permutations de 10! à essayer. Mais pour base-11, les problèmes prenaient quelques minutes et ceux de base-12 prenaient presque une heure.

J'ai remplacé l'algorithme d'énumération de permutation que j'avais utilisé par un simple i=0--to--N-1 pour-boucle, en utilisant l'algorithme cité par Permaquid. Le résultat n'était que légèrement plus lent. Mais ensuite, j'ai divisé la plage entière en trimestres et exécuté quatre boucles simultanément, chacune dans un thread séparé. Sur mon processeur quad-core, le programme résultant a été exécuté quatre fois plus vite.

Tout comme il est difficile de trouver une permutation individuelle à l'aide des algorithmes de dénombrement par permutation, il est également difficile de générer des sous-ensembles délimités de l'ensemble des permutations. L'algorithme cité par Permaquid rend ces deux choses très faciles

23
Jeff Dege

En particulier, vous voulez std :: next_permutation .

void permute(string elems, int mid, int end)
{
  int count = 0;
  while(next_permutation(elems.begin()+mid, elems.end()))
    cout << << ++count << " : " << elems << endl;
}

... ou quelque chose comme ça...

11
Kornel Kisielewicz

L’algorithme de mélange aléatoire Knuth mérite d’être examiné.

// In-place shuffle of char array
void shuffle(char array[], int n)
{
    for ( ; n > 1; n--)
    {
        // Pick a random element to move to the end
        int k = Rand() % n;  // 0 <= k <= n-1  

        // Simple swap of variables
        char tmp = array[k];
        array[k] = array[n-1];
        array[n-1] = tmp;
    }
}
4
David R Tribble

Tout algorithme permettant de générer des permutations va s'exécuter en temps polynomial, car le nombre de permutations pour les caractères dans une chaîne de longueur n est (n!). Cela dit, il existe des algorithmes sur place assez simples pour générer des permutations. Découvrez l'algorithme Johnson-Trotter .

4
JohnE

Tout algorithme qui utilise ou génère toutes les permutations prendra O (N! * N) temps, O (N!) Au moins pour générer toutes les permutations et O(N) pour utiliser le résultat, et c'est vraiment lent. Notez que l'impression de la chaîne est également O(N) autant que je sache.

En une seconde, vous ne pouvez gérer de manière réaliste que des chaînes d'un maximum de 10 ou 11 caractères, quelle que soit la méthode utilisée. Depuis 11! * 11 = 439084800 itérations (le faire en une seconde sur la plupart des machines le fait avancer) et 12! * 12 = 5748019200 itérations. Ainsi, même la mise en œuvre la plus rapide prendrait environ 30 à 60 secondes sur 12 caractères. 

Factorial grossit trop vite pour que vous espériez gagner quelque chose en écrivant une implémentation plus rapide, vous gagneriez tout au plus un personnage. Je suggérerais donc la recommandation de Prasoon. C'est facile à coder et c'est assez rapide. Bien que s'en tenir à votre code est tout à fait bien aussi.

Je vous recommande simplement de veiller à ne pas ajouter par inadvertance de caractères supplémentaires à votre chaîne, tels que le caractère null. Puisque cela fera votre code un facteur de N plus lent.

3
JPvdMerwe

Je ne pense pas que cela soit meilleur, mais cela fonctionne et n'utilise pas de récursivité:

#include <iostream>
#include <stdexcept>
#include <tr1/cstdint>

::std::uint64_t fact(unsigned int v)
{
   ::std::uint64_t output = 1;
   for (unsigned int i = 2; i <= v; ++i) {
      output *= i;
   }
   return output;
}

void permute(const ::std::string &s)
{
   using ::std::cout;
   using ::std::uint64_t;
   typedef ::std::string::size_type size_t;

   static unsigned int max_size = 20;  // 21! > 2^64

   const size_t strsize = s.size();

   if (strsize > max_size) {
      throw ::std::overflow_error("This function can only permute strings of size 20 or less.");
   } else if (strsize < 1) {
      return;
   } else if (strsize == 1) {
      cout << "0 : " << s << '\n';
   } else {
      const uint64_t num_perms = fact(s.size());
      // Go through each permutation one-by-one
      for (uint64_t perm = 0; perm < num_perms; ++perm) {
         // The indexes of the original characters in the new permutation
         size_t idxs[max_size];

         // The indexes of the original characters in the new permutation in
         // terms of the list remaining after the first n characters are pulled
         // out.
         size_t residuals[max_size];

         // We use div to pull our permutation number apart into a set of
         // indexes.  This holds what's left of the permutation number.
         uint64_t permleft = perm;

         // For a given permutation figure out which character from the original
         // goes in each slot in the new permutation.  We start assuming that
         // any character could go in any slot, then narrow it down to the
         // remaining characters with each step.
         for (unsigned int i = strsize; i > 0; permleft /= i, --i) {
            uint64_t taken_char = permleft % i;
            residuals[strsize - i] = taken_char;

            // Translate indexes in terms of the list of remaining characters
            // into indexes in terms of the original string.
            for (unsigned int o = (strsize - i); o > 0; --o) {
               if (taken_char >= residuals[o - 1]) {
                  ++taken_char;
               }
            }
            idxs[strsize - i] = taken_char;
         }
         cout << perm << " : ";
         for (unsigned int i = 0; i < strsize; ++i) {
            cout << s[idxs[i]];
         }
         cout << '\n';
      }
   }
}

La chose amusante à propos de cela est que le seul état qu’il utilise de permutation à permutation est le numéro de la permutation, le nombre total de permutations et la chaîne originale. Cela signifie qu'il peut être facilement encapsulé dans un itérateur ou quelque chose du genre sans devoir conserver soigneusement l'état exact correct. Il peut même s'agir d'un itérateur à accès aléatoire.

Bien sûr, :: std :: next_permutation stocke l'état dans les relations entre les éléments, mais cela signifie que cela ne peut pas fonctionner sur des choses non ordonnées, et je me demanderais vraiment ce qu'il en est si vous avez deux choses égales dans la séquence. Vous pouvez résoudre ce problème en permutant les index, bien sûr, mais cela ajoute un peu plus de complications.

Mine fonctionnera avec n'importe quelle plage d'itérateurs à accès aléatoire, à condition qu'elle soit suffisamment courte. Et si ce n'est pas le cas, vous ne traverserez jamais toutes les permutations.

L'idée de base de cet algorithme est que chaque permutation de N éléments peut être énumérée. Le nombre total est N! ou fact(N). Et toute permutation donnée peut être considérée comme un mappage des indices source de la séquence originale en un ensemble d’indices de destination dans la nouvelle séquence. Une fois que vous avez énuméré toutes les permutations, il ne reste plus qu'à mapper chaque numéro de permutation en une permutation réelle.

Le premier élément de la liste permutée peut être l'un des N éléments de la liste d'origine. Le deuxième élément peut être l’un des N-1 éléments restants, etc. L'algorithme utilise l'opérateur % pour séparer le nombre de permutation en un ensemble de sélections de cette nature. D'abord, modulo est le nombre de permutation par N pour obtenir un nombre de [0, N). Il rejette le reste en divisant par N, puis modulo par la taille de la liste - 1 pour obtenir un nombre compris entre [0, N-1) et ainsi de suite. C'est ce que fait la boucle for (i =.

La deuxième étape consiste à traduire chaque numéro en un index dans la liste d'origine. Le premier numéro est facile car il s’agit d’un index simple. Le second numéro est un index dans une liste qui contient tous les éléments sauf celui supprimé au premier index, etc. C'est ce que fait la boucle for (o =.

residuals est une liste d'index dans les listes de plus en plus petites. idxs est une liste d'index dans la liste d'origine. Il existe un mappage un-un entre les valeurs dans residuals et idxs. Ils représentent chacun la même valeur dans différents «espaces de coordonnées».

La réponse indiquée par la réponse que vous avez choisie a la même idée de base, mais présente une manière beaucoup plus élégante de réaliser la cartographie que ma méthode plutôt littérale et par la force brute. Cette méthode sera légèrement plus rapide que ma méthode, mais elles ont à peu près la même vitesse et ont le même avantage d’accès aléatoire dans l’espace de permutation, ce qui facilite un grand nombre de choses, y compris (comme le soulignait la réponse que vous avez choisie). algorithmes parallèles.

1
Omnifarious

Voulez-vous parcourir toutes les permutations ou compter le nombre de permutations?

Pour les premiers, utilisez std::next_permutation comme suggéré par d’autres. Chaque permutation prend O(N) temps (mais moins de temps amorti) et aucune mémoire sauf sa callframe, par rapport à O(N) temps et O(N) mémoire pour votre fonction récursive. Tout le processus est O (N!) Et vous ne pouvez pas faire mieux que cela, comme d’autres l'ont dit, car vous ne pouvez obtenir plus que O(X) résultats d'un programme en moins de O(X) le temps! De toute façon, sans ordinateur quantique.

Pour ces derniers, il vous suffit de savoir combien d'éléments uniques sont dans la chaîne.

big_int count_permutations( string s ) {
    big_int divisor = 1;
    sort( s.begin(), s.end() );
    for ( string::iterator pen = s.begin(); pen != s.end(); ) {
        size_t cnt = 0;
        char value = * pen;
        while ( pen != s.end() && * pen == value ) ++ cnt, ++ pen;
        divisor *= big_int::factorial( cnt );
    }
    return big_int::factorial( s.size() ) / divisor;
}

La vitesse est limitée par l'opération consistant à rechercher les éléments en double, ce qui peut être fait pour chars en O(N) avec une table de correspondance.

1
Potatoswatter

Le uniquement moyen d'améliorer considérablement les performances est de trouver un moyen d'éviter de parcourir toutes les permutations en premier lieu!

Permuter est une opération inévitablement lente (O (n!), Ou pire, en fonction de ce que vous faites avec chaque permutation), malheureusement rien que vous ne puissiez faire ne changera ce fait.

Notez également que tout compilateur moderne aplatira votre récursion lorsque les optimisations seront activées, de sorte que les gains (faibles) en performances résultant de l'optimisation manuelle seront encore réduits.

1
James

J'ai récemment écrit un algorithme de permutation. Il utilise un vecteur de type T (modèle) au lieu d'une chaîne, et ce n'est pas très rapide, car il utilise la récursivité et permet beaucoup de copies. Mais peut-être pouvez-vous vous inspirer du code. Vous pouvez trouver le code ici .

1
StackedCrooked

En fait, vous pouvez le faire en utilisant Knuth Shuffling Algo!

// find all the permutations of a string
// using Knuth radnom shuffling algorithm!

#include <iostream>
#include <string>

template <typename T, class Func>
void permutation(T array, std::size_t N, Func func)
{
    func(array);
    for (std::size_t n = N-1; n > 0; --n)
    {
        for (std::size_t k = 0; k <= n; ++k)
        {
            if (array[k] == array[n]) continue;
            using std::swap;
            swap(array[k], array[n]);
            func(array);
        }
    }
}

int main()
{
    while (std::cin.good())
    {
        std::string str;
        std::cin >> str;
        permutation(str, str.length(), [](std::string const &s){ 
            std::cout << s << std::endl; });
    }
}
1
Reza Toghraee

This post: http://cplusplus.co.il/2009/11/14/enumerating-permutations/ traite de la permutation de tout, pas seulement des chaînes. Le post lui-même et les commentaires ci-dessous sont assez instructifs et je ne voudrais pas copier/coller ..

0
rmn

En voici un que je viens de balbutier !!

void permute(const char* str, int level=0, bool print=true) {

    if (print) std::cout << str << std::endl;

    char temp[30];
    for (int i = level; i<strlen(str); i++) {

        strcpy(temp, str);

        temp[level] = str[i];
        temp[i] = str[level];

        permute(temp, level+1, level!=i);
    }
}

int main() {
    permute("1234");

    return 0;
}
0
Rich

Voici encore une autre fonction récursive pour les permutations de chaînes:

void permute(string prefix, string suffix, vector<string> &res) {
    if (suffix.size() < 1) {
        res.Push_back(prefix);
        return;
    }
    for (size_t i = 0; i < suffix.size(); i++) {
        permute(prefix + suffix[i], suffix.substr(0,i) + suffix.substr(i + 1), res);
    }
}


int main(){
    string str = "123";
    vector<string> res;
    permute("", str, res);
}

La fonction collecte toutes les permutations dans le vecteur res . L'idée peut être généralisée pour différents types de conteneurs à l'aide de modèles et d'itérateurs:

template <typename Cont1_t, typename Cont2_t>
void permute(typename Cont1_t prefix,
    typename Cont1_t::iterator beg, typename Cont1_t::iterator end,
    Cont2_t &result)
{
    if (beg == end) {
        result.insert(result.end(), prefix);
        return;
    }
    for (auto it = beg; it != end; ++it) {
        prefix.insert(prefix.end(), *it);
        Cont1_t tmp;
        for (auto i = beg; i != end; ++i)
            if (i != it)
                tmp.insert(tmp.end(), *i);

        permute(prefix, tmp.begin(), tmp.end(), result);
        prefix.erase(std::prev(prefix.end()));
    }
}

int main()
{
    string str = "123";
    vector<string> rStr;
    permute<string, vector<string>>("", str.begin(), str.end(), rStr);

    vector<int>vint = { 1,2,3 };
    vector<vector<int>> rInt;
    permute<vector<int>, vector<vector<int>>>({}, vint.begin(), vint.end(), rInt);

    list<long> ll = { 1,2,3 };
    vector<list<long>> vlist;
    permute<list<long>, vector<list<long>>>({}, ll.begin(), ll.end(), vlist);
}

Cela peut être un exercice de programmation intéressant, mais dans le code de production, vous devez utiliser une version non permutable de la permutation, comme next_permutation.

0
**// Prints all permutation of a string**

    #include<bits/stdc++.h>
    using namespace std;


    void printPermutations(string input, string output){
        if(input.length() == 0){
            cout<<output <<endl;
            return;
        }

        for(int i=0; i<=output.length(); i++){
            printPermutations(input.substr(1),  output.substr(0,i) + input[0] + output.substr(i));
        }
    }

    int main(){
        string s = "ABC";
        printPermutations(s, "");
        return 0;
    }
0
Deepak Singh

Même moi, j’ai eu du mal à comprendre cette version récursive de la première fois et il m’a fallu du temps pour rechercher une méthode plus facile. La meilleure méthode pour trouver (à quoi je puisse penser) est d’utiliser l’algorithme proposé par Narayana Pandita . L'idée de base est:

  1. Commencez par trier la chaîne donnée dans l’ordre non décroissant, puis recherchez l’index du premier élément de la fin inférieur à son caractère suivant, lexicigraphiquement. Appelez cet élément index le 'firstIndex'.
  2. Maintenant, trouvez le plus petit caractère qui soit plus grand que l’élément au premier index. Appelez cet élément index le 'ceilIndex'.
  3. Maintenant, permutez les éléments dans 'firstIndex' et 'ceilIndex'.
  4. Inversez la partie de la chaîne en commençant par l'index 'firstIndex + 1' jusqu'à la fin de la chaîne.
  5. (Au lieu du point 4) Vous pouvez également trier la partie de la chaîne de l'index 'firstIndex + 1' à la fin de la chaîne.

Les points 4 et 5 font la même chose mais la complexité temporelle dans le cas du point 4 est O (n * n!) Et celle dans le cas du point 5 est O (n ^ 2 * n!).

L'algorithme ci-dessus peut même être appliqué au cas où nous avons des caractères en double dans la chaîne. : 

Le code pour afficher toute la permutation d'une chaîne: 

#include <iostream>

using namespace std;

void swap(char *a, char *b)
{
    char tmp = *a;
    *a = *b;
    *b = tmp;
}


int partition(char arr[], int start, int end)
{
    int x = arr[end];
    int i = start - 1;
    for(int j = start; j <= end-1; j++)
    {
        if(arr[j] <= x)
        {
            i = i + 1;
            swap(&arr[i], &arr[j]);
        }
    }
    swap(&arr[i+1], &arr[end]);
    return i+1;
}

void quickSort(char arr[], int start, int end)
{
    if(start<end)
    {
        int q = partition(arr, start, end);
        quickSort(arr, start, q-1);
        quickSort(arr, q+1, end);
    }
}

int findCeilIndex(char *str, int firstIndex, int n)
{
    int ceilIndex;
    ceilIndex = firstIndex+1;

    for (int i = ceilIndex+1; i < n; i++)
    {
        if(str[i] >= str[firstIndex] && str[i] <= str[ceilIndex])
            ceilIndex = i;
    }
    return ceilIndex;
}

void reverse(char *str, int start, int end)
{
    while(start<=end)
    {
        char tmp = str[start];
        str[start] = str[end];
        str[end] = tmp;
        start++;
        end--;
    }
}

void permutate(char *str, int n)
{
    quickSort(str, 0, n-1);
    cout << str << endl;
    bool done = false;
    while(!done)
    {
        int firstIndex;
        for(firstIndex = n-2; firstIndex >=0; firstIndex--)
        {
            if(str[firstIndex] < str[firstIndex+1])
                break;
        }
        if(firstIndex<0)
            done = true;
        if(!done)
        {
            int ceilIndex;
            ceilIndex = findCeilIndex(str, firstIndex, n);
            swap(&str[firstIndex], &str[ceilIndex]);
            reverse(str, firstIndex+1, n-1);
            cout << str << endl;
        }
    }
}


int main()
{
    char str[] = "mmd";
    permutate(str, 3);
    return 0;
}
0
Lokesh Basu

Si vous êtes intéressé par la génération de permutation, j'ai déjà rédigé un article de recherche à ce sujet: http://www.oriontransfer.co.nz/research/permutation-generation

Il vient complet avec le code source, et il y a environ 5 méthodes différentes implémentées.

0
ioquatix

Ce n'est pas la meilleure logique, mais alors, je suis débutant. Je serai très heureux et obligé si quelqu'un me donne des suggestions sur ce code

#include<iostream.h>
#include<conio.h>
#include<string.h>
int c=1,j=1;


int fact(int p,int l)
{
int f=1;
for(j=1;j<=l;j++)
{
f=f*j;
if(f==p)
return 1;

}
return 0;
}


void rev(char *a,int q)
{
int l=strlen(a);
int m=l-q;
char t;
for(int x=m,y=0;x<q/2+m;x++,y++)
{
t=a[x];
a[x]=a[l-y-1];
a[l-y-1]=t;
}
c++;
cout<<a<<"  ";
}

int perm(char *a,int f,int cd)
{
if(c!=f)
{
int l=strlen(a);
rev(a,2);
cd++;
if(c==f)return 0;
if(cd*2==6)
{
for(int i=1;i<=c;i++)
{
if(fact(c/i,l)==1)
{
rev(a,j+1);
rev(a,2);
break;
}
}
cd=1;
}
rev(a,3);
perm(a,f,cd);
}
return 0;
}

void main()
{
clrscr();
char *a;
cout<<"\n\tEnter a Word";
cin>>a;
int f=1;

for(int o=1;o<=strlen(a);o++)
f=f*o;

perm(a,f,0);
getch();
}
0
Ayosh Maitra