std :: next_permutation Implementation Explanation


110

Ero curioso di sapere come std:next_permutationfosse implementato così ho estratto la gnu libstdc++ 4.7versione e disinfettato gli identificatori e la formattazione per produrre la seguente demo ...

#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()));
}

L'output è come previsto: http://ideone.com/4nZdx

Le mie domande sono: come funziona? Qual è il significato di i, je k? Che valore hanno nelle diverse parti dell'esecuzione? Che cos'è uno schizzo di una prova della sua correttezza?

Chiaramente prima di entrare nel ciclo principale, controlla solo i casi banali dell'elenco di elementi 0 o 1. All'ingresso del ciclo principale i punta all'ultimo elemento (non una fine passata) e l'elenco è lungo almeno 2 elementi.

Cosa sta succedendo nel corpo del ciclo principale?


Ehi, come hai estratto quel pezzo di codice? Quando ho controllato #include <algorithm>, il codice era completamente diverso che consisteva in più funzioni
Manjunath

Risposte:


172

Diamo un'occhiata ad alcune permutazioni:

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
...

Come si passa da una permutazione all'altra? In primo luogo, guardiamo le cose in modo leggermente diverso. Possiamo visualizzare gli elementi come cifre e le permutazioni come numeri . Considerando il problema in questo modo , vogliamo ordinare le permutazioni / numeri in ordine "crescente" .

Quando ordiniamo i numeri, vogliamo "aumentarli del minimo". Ad esempio, quando contiamo non contiamo 1, 2, 3, 10, ... perché ci sono ancora 4, 5, ... in mezzo e sebbene 10 sia maggiore di 3, ci sono numeri mancanti che possono essere ottenuti da aumentando 3 di una quantità minore. Nell'esempio sopra vediamo che 1rimane come il primo numero per un lungo periodo di tempo poiché ci sono molti riordinamenti delle ultime 3 "cifre" che "aumentano" la permutazione di un importo inferiore.

Allora quando finalmente "usiamo" il 1? Quando non ci sono più permutazioni delle ultime 3 cifre.
E quando non ci sono più permutazioni delle ultime 3 cifre? Quando le ultime 3 cifre sono in ordine decrescente.

Aha! Questa è la chiave per comprendere l'algoritmo. Modifichiamo solo la posizione di una "cifra" quando tutto a destra è in ordine decrescente perché se non è in ordine decrescente ci sono ancora più permutazioni da fare (cioè possiamo "aumentare" la permutazione di una quantità minore) .

Torniamo ora al codice:

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

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

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

Dalle prime 2 righe del ciclo, jè un elemento ed iè l'elemento prima di esso.
Quindi, se gli elementi sono in ordine crescente, ( if (*i < *j)) fa qualcosa.
Altrimenti, se l'intera cosa è in ordine decrescente, ( if (i == begin)) allora questa è l'ultima permutazione.
Altrimenti, continuiamo e vediamo che j ed i sono essenzialmente decrementati.

Ora comprendiamo la if (i == begin)parte, quindi tutto ciò che dobbiamo capire è la if (*i < *j)parte.

Nota anche: "Allora se gli elementi sono in ordine crescente ..." che supporta la nostra precedente osservazione che dobbiamo solo fare qualcosa su una cifra "quando tutto a destra è in ordine decrescente". La ifdichiarazione di ordine ascendente sta essenzialmente trovando il punto più a sinistra dove "tutto a destra è in ordine decrescente".

Vediamo di nuovo alcuni esempi:

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

Vediamo che quando tutto a destra di una cifra è in ordine decrescente, troviamo la cifra successiva più grande e la mettiamo davanti, quindi mettiamo le cifre rimanenti in ordine crescente .

Diamo un'occhiata al codice:

It k = end;

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

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

Bene, poiché le cose a destra sono in ordine decrescente, per trovare la "cifra più grande successiva" dobbiamo solo iterare dalla fine, che vediamo nelle prime 3 righe di codice.

Successivamente, scambiamo la "cifra successiva più grande" in primo piano con l' iter_swap()istruzione e quindi poiché sappiamo che quella cifra era la successiva più grande, sappiamo che le cifre a destra sono ancora in ordine decrescente, quindi per metterla in ordine crescente, dobbiamo solo reverse()farlo.


12
Incredibile spiegazione

2
Grazie per la spiegazione! Questo algoritmo è chiamato generazione in ordine lessicografico . Ci sono numeri di tale algoritmo in Combinatorics, ma questo è il più classico.
catena ro

1
Qual è la complessità di tale algoritmo?
user72708

leetcode ha una buona spiegazione, leetcode.com/problems/next-permutation/solution
bicepjai

40

L'implementazione di gcc genera permutazioni in ordine lessicografico. Wikipedia lo spiega come segue:

Il seguente algoritmo genera la successiva permutazione lessicograficamente dopo una data permutazione. Cambia la permutazione data sul posto.

  1. Trova il più grande indice k tale che a [k] <a [k + 1]. Se non esiste tale indice, la permutazione è l'ultima permutazione.
  2. Trova l'indice più grande l tale che a [k] <a [l]. Poiché k + 1 è un tale indice, l è ben definito e soddisfa k <l.
  3. Scambia a [k] con a [l].
  4. Invertire la sequenza da a [k + 1] fino a includere l'elemento finale a [n].

AFAICT, tutte le implementazioni generano lo stesso ordine.
MSalters

12

Knuth approfondisce questo algoritmo e le sue generalizzazioni nelle sezioni 7.2.1.2 e 7.2.1.3 di The Art of Computer Programming . Lo chiama "Algorithm L" - a quanto pare risale al XIII secolo.


1
Puoi menzionare il nome del libro?
Grobber

3
TAOCP = The Art of Computer Programming

9

Ecco un'implementazione completa utilizzando altri algoritmi di libreria 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 rsorted_end = std::is_sorted_until(rbegin, rend, comp);
    bool has_more_permutations = rsorted_end != rend;
    if (has_more_permutations) {
        auto next_permutation_rend = std::upper_bound(
            rbegin, rsorted_end, *rsorted_end, comp);
        std::iter_swap(rsorted_end, next_permutation_rend);
    }
    std::reverse(rbegin, rsorted_end);
    return has_more_permutations;
}

dimostrazione


1
Ciò sottolinea l'importanza di buoni nomi di variabili e la separazione delle preoccupazioni. is_final_permutationè più informativo di begin == end - 1. La chiamata is_sorted_until/ upper_boundsepara la logica di permutazione da quelle operazioni e rende questo molto più comprensibile. Inoltre upper_bound è una ricerca binaria, mentre while (!(*i < *--k));è lineare, quindi è più performante.
Jonathan Gawrych

1

Esiste una possibile implementazione autoesplicativa sull'utilizzo di cppreference<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;
        }
    }
}

Modificare il contenuto in permutazione lessicograficamente successiva (sul posto) e restituire true se esiste altrimenti ordinare e restituire false se non esiste.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.