web-dev-qa-db-fra.com

Explication de l'implémentation de std :: next_permutation

J'étais curieux de voir comment std:next_permutation a été implémenté, j'ai donc extrait le gnu libstdc++ 4.7 version et a nettoyé les identifiants et le formatage pour produire la démo suivante ...

#include <vector>
#include <iostream>
#include <algorithm>

using namespace std;

template<typename It>
bool next_permutation(It begin, It end)
{
        if (begin == end)
                return false;

        It i = begin;
        ++i;
        if (i == end)
                return false;

        i = end;
        --i;

        while (true)
        {
                It j = i;
                --i;

                if (*i < *j)
                {
                        It k = end;

                        while (!(*i < *--k))
                                /* pass */;

                        iter_swap(i, k);
                        reverse(j, end);
                        return true;
                }

                if (i == begin)
                {
                        reverse(begin, end);
                        return false;
                }
        }
}

int main()
{
        vector<int> v = { 1, 2, 3, 4 };

        do
        {
                for (int i = 0; i < 4; i++)
                {
                        cout << v[i] << " ";
                }
                cout << endl;
        }
        while (::next_permutation(v.begin(), v.end()));
}

La sortie est comme prévu: http://ideone.com/4nZdx

Mes questions sont: comment ça marche? Quelle est la signification de i, j et k? Quelle valeur ont-ils dans les différentes parties de l'exécution? Qu'est-ce qu'une esquisse d'une preuve de son exactitude?

De toute évidence, avant d'entrer dans la boucle principale, il vérifie simplement les cas de liste d'éléments 0 ou 1 triviaux. À l'entrée de la boucle principale, i pointe vers le dernier élément (pas une fin passée) et la liste est longue d'au moins 2 éléments.

Que se passe-t-il dans le corps de la boucle principale?

99
Andrew Tomazos

Regardons quelques permutations:

1 2 3 4
1 2 4 3
1 3 2 4
1 3 4 2
1 4 2 3
1 4 3 2
2 1 3 4
...

Comment passer d'une permutation à l'autre? Tout d'abord, regardons les choses un peu différemment. Nous pouvons voir les éléments sous forme de chiffres et les permutations sous forme de nombres. Voir le problème de cette façon nous voulons ordonner les permutations/nombres dans l'ordre "croissant".

Lorsque nous commandons des numéros, nous voulons "les augmenter du plus petit montant". Par exemple, lors du comptage, nous ne comptons pas 1, 2, 3, 10, ... car il y a toujours 4, 5, ... entre les deux et bien que 10 soit plus grand que 3, il y a des nombres manquants qui peuvent être obtenus par en augmentant de 3 d'un montant inférieur. Dans l'exemple ci-dessus, nous voyons que 1 Reste le premier nombre pendant longtemps car il y a beaucoup de réorganisations des 3 derniers "chiffres" qui "augmentent" la permutation d'un montant plus petit.

Alors, quand "utilisons-nous" finalement le 1? Lorsqu'il n'y a plus de permutations des 3 derniers chiffres.
Et quand n'y a-t-il plus de permutations des 3 derniers chiffres? Lorsque les 3 derniers chiffres sont en ordre décroissant.

Ah! C'est la clé pour comprendre l'algorithme. Nous ne changeons la position d'un "chiffre" que lorsque tout à droite est en ordre décroissant parce que s'il n'est pas en ordre décroissant, il y a encore plus de permutations aller (c'est-à-dire que nous pouvons "augmenter" la permutation d'un montant plus petit).

Revenons maintenant au code:

while (true)
{
    It j = i;
    --i;

    if (*i < *j)
    { // ...
    }

    if (i == begin)
    { // ...
    }
}

À partir des 2 premières lignes de la boucle, j est un élément et i est l'élément qui le précède.
Ensuite, si les éléments sont dans l'ordre croissant, (if (*i < *j)) fait quelque chose.
Sinon, si le tout est en ordre décroissant, (if (i == begin)) alors c'est la dernière permutation.
Sinon, nous continuons et nous voyons que j et i sont essentiellement décrémentés.

Nous comprenons maintenant la partie if (i == begin) donc tout ce que nous devons comprendre est la partie if (*i < *j).

Notez également: "Alors, si les éléments sont en ordre croissant ...", ce qui confirme notre observation précédente selon laquelle nous n'avons besoin de faire quelque chose qu'à un chiffre "lorsque tout à droite est en ordre décroissant". L'instruction d'ordre croissant if trouve essentiellement l'endroit le plus à gauche où "tout à droite est en ordre décroissant".

Regardons à nouveau quelques exemples:

...
1 4 3 2
2 1 3 4
...
2 4 3 1
3 1 2 4
...

Nous voyons que lorsque tout à droite d'un chiffre est en ordre décroissant, nous trouver le prochain plus grand chiffre et le mettre devant puis mettre les autres chiffres en ordre croissant .

Regardons le code:

It k = end;

while (!(*i < *--k))
    /* pass */;

iter_swap(i, k);
reverse(j, end);
return true;

Eh bien, puisque les choses à droite sont en ordre décroissant, pour trouver le "prochain chiffre le plus grand", il suffit d'itérer à partir de la fin, ce que nous voyons dans les 3 premières lignes de code.

Ensuite, nous échangeons le "prochain plus grand chiffre" vers l'avant avec l'instruction iter_swap() puis, puisque nous savons que le chiffre était le prochain plus grand, nous savons que les chiffres à droite sont toujours en ordre décroissant, donc pour le mettre dans l'ordre croissant, il suffit de le reverse().

160
flight

L'implémentation gcc génère des permutations dans l'ordre lexicographique. Wikipedia l'explique comme suit:

L'algorithme suivant génère la permutation suivante lexicographiquement après une permutation donnée. Il modifie la permutation donnée en place.

  1. Trouvez le plus grand indice k tel que a [k] <a [k + 1]. S'il n'existe aucun indice de ce type, la permutation est la dernière permutation.
  2. Trouvez le plus grand indice l tel que a [k] <a [l]. Puisque k + 1 est un tel indice, l est bien défini et satisfait k <l.
  3. Échangez un [k] avec un [l].
  4. Inverse la séquence de a [k + 1] jusqu'à et y compris l'élément final a [n].
36
TemplateRex

Knuth approfondit cet algorithme et ses généralisations dans les sections 7.2.1.2 et 7.2.1.3 de The Art of Computer Programming. Il l'appelle "Algorithme L" - apparemment, il remonte au 13ème siècle.

12
user755921

Voici une implémentation complète utilisant d'autres algorithmes de bibliothèque standard:

template <typename I, typename C>
    // requires BidirectionalIterator<I> && Compare<C>
bool my_next_permutation(I begin, I end, C comp) {
    auto rbegin = std::make_reverse_iterator(end);
    auto rend = std::make_reverse_iterator(begin);
    auto next_unsorted = std::is_sorted_until(rbegin, rend, comp);
    bool at_final_permutation = (next_unsorted == rend);
    if (!at_final_permutation) {
        auto next_permutation = std::upper_bound(
            rbegin, next_unsorted, *next_unsorted, comp);
        std::iter_swap(next_unsorted, next_permutation);
    }
    std::reverse(rbegin, next_unsorted);
    return !at_final_permutation;
}

Démo

7
Brian Rodriguez

Il existe une implémentation possible explicite sur cppreference utilisant <algorithm>.

template <class Iterator>
bool next_permutation(Iterator first, Iterator last) {
    if (first == last) return false;
    Iterator i = last;
    if (first == --i) return false;
    while (1) {
        Iterator i1 = i, i2;
        if (*--i < *i1) {
            i2 = last;
            while (!(*i < *--i2));
            std::iter_swap(i, i2);
            std::reverse(i1, last);
            return true;
        }
        if (i == first) {
            std::reverse(first, last);
            return false;
        }
    }
}

Changez le contenu en permutation lexicographique suivante (sur place) et retournez vrai s'il existe sinon triez et retournez faux s'il n'existe pas.

1
Shreevardhan