std :: next_permutation Implementación Explicación

110

Tenía curiosidad por saber cómo std:next_permutationse implementó, así que extraje la gnu libstdc++ 4.7versión y desinfecte los identificadores y el formato para producir la siguiente demostración ...

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

El resultado es el esperado: http://ideone.com/4nZdx

Mis preguntas son: ¿Cómo funciona? ¿Cuál es el significado de i, jy k? ¿Qué valor tienen en las diferentes partes de la ejecución? ¿Qué es un bosquejo de una prueba de su corrección?

Claramente, antes de ingresar al ciclo principal, solo verifica los casos triviales de lista de elementos 0 o 1. En la entrada del bucle principal, i está apuntando al último elemento (no a un final pasado) y la lista tiene al menos 2 elementos.

¿Qué está pasando en el cuerpo del bucle principal?

Andrés Tomazos
fuente
Oye, ¿cómo extrajiste ese fragmento de código? Cuando verifiqué #include <algorithm>, el código era completamente diferente, que consistía en más funciones
Manjunath

Respuestas:

172

Veamos algunas permutaciones:

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

¿Cómo pasamos de una permutación a la siguiente? En primer lugar, veamos las cosas de manera un poco diferente. Podemos ver los elementos como dígitos y las permutaciones como números . Viendo el problema de esta manera , queremos ordenar las permutaciones / números en orden "ascendente". .

Cuando ordenamos números queremos "aumentarlos en la cantidad más pequeña". Por ejemplo, cuando contamos no contamos 1, 2, 3, 10, ... porque todavía hay 4, 5, ... en el medio y aunque 10 es más grande que 3, faltan números que pueden obtenerse por aumentando 3 en una cantidad menor. En el ejemplo anterior vemos que 1permanece como el primer número durante mucho tiempo ya que hay muchos reordenamientos de los últimos 3 "dígitos" que "aumentan" la permutación en una cantidad menor.

Entonces, ¿cuándo finalmente "usamos" el 1? Cuando no haya más permutaciones de los últimos 3 dígitos.
¿Y cuándo no hay más permutaciones de los últimos 3 dígitos? Cuando los últimos 3 dígitos están en orden descendente.

¡Ajá! Esta es la clave para comprender el algoritmo. Solo cambiamos la posición de un "dígito" cuando todo a la derecha está en orden descendente porque si no está en orden descendente, todavía quedan más permutaciones (es decir, podemos "aumentar" la permutación en una cantidad menor) .

Volvamos ahora al código:

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

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

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

De las 2 primeras líneas del bucle, jes un elemento y ies el elemento anterior.
Luego, si los elementos están en orden ascendente, ( if (*i < *j)) haga algo.
De lo contrario, si todo está en orden descendente, ( if (i == begin)) entonces esta es la última permutación.
De lo contrario, continuamos y vemos que j e i se reducen esencialmente.

Ahora entendemos la if (i == begin)parte, así que todo lo que necesitamos entender es laif (*i < *j) parte.

También tenga en cuenta: "Entonces, si los elementos están en orden ascendente ...", lo que respalda nuestra observación anterior de que solo necesitamos hacer algo con un dígito "cuando todo a la derecha está en orden descendente". La ifdeclaración de orden ascendente es esencialmente encontrar el lugar más a la izquierda donde "todo a la derecha está en orden descendente".

Veamos nuevamente algunos ejemplos:

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

Vemos que cuando todo a la derecha de un dígito está en orden descendente, buscamos el siguiente dígito más grande y lo colocamos al frente y luego colocamos los dígitos restantes en orden ascendente .

Veamos el código:

It k = end;

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

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

Bueno, dado que las cosas a la derecha están en orden descendente, para encontrar el "siguiente dígito más grande" solo tenemos que iterar desde el final, que vemos en las primeras 3 líneas de código.

A continuación, intercambiamos el "siguiente dígito más grande" al frente con la iter_swap()declaración y luego, como sabemos que ese dígito era el siguiente más grande, sabemos que los dígitos de la derecha todavía están en orden descendente, así que para ponerlos en orden ascendente, solo tenemos que reverse()hacerlo.

vuelo
fuente
12
Increíble explicación
2
¡Gracias por la explicación! Este algoritmo se llama Generación en orden lexicográfico . Hay números de dicho algoritmo Combinatorics, pero este es el más clásico.
cadena ro
1
¿Cuál es la complejidad de tal algoritmo?
user72708
leetcode tiene una buena explicación, leetcode.com/problems/next-permutation/solution
bicepjai
40

La implementación de gcc genera permutaciones en orden lexicográfico. Wikipedia lo explica de la siguiente manera:

El siguiente algoritmo genera la siguiente permutación lexicográficamente después de una determinada permutación. Cambia la permutación dada en el lugar.

  1. Encuentre el índice k más grande tal que a [k] <a [k + 1]. Si no existe tal índice, la permutación es la última permutación.
  2. Encuentre el índice más grande l tal que a [k] <a [l]. Dado que k + 1 es un índice de este tipo, l está bien definido y satisface k <l.
  3. Cambie una [k] por una [l].
  4. Invierta la secuencia desde a [k + 1] hasta el elemento final a [n] inclusive.
TemplateRex
fuente
AFAICT, todas las implementaciones generan el mismo orden.
MSalters
12

Knuth profundiza sobre este algoritmo y sus generalizaciones en las secciones 7.2.1.2 y 7.2.1.3 de El arte de la programación informática . Lo llama "Algoritmo L", aparentemente se remonta al siglo XIII.

rvighne
fuente
1
¿Puedes mencionar el nombre del libro?
Grobber
3
TAOCP = El arte de la programación informática
9

Aquí hay una implementación completa utilizando otros algoritmos de biblioteca estándar:

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;
}

Manifestación

Brian Rodríguez
fuente
1
Esto subraya la importancia de buenos nombres de variables y separación de preocupaciones. is_final_permutationes más informativo que begin == end - 1. Llamar is_sorted_until/ upper_boundsepara la lógica de permutación de esas operaciones y hace que esto sea mucho más comprensible. Además, upper_bound es una búsqueda binaria, mientras que while (!(*i < *--k));es lineal, por lo que es más eficaz .
Jonathan Gawrych
1

Hay una posible implementación autoexplicativa sobre el uso de 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;
        }
    }
}

Cambie el contenido a la siguiente permutación lexicográfica (en el lugar) y devuelva verdadero si existe; de ​​lo contrario, ordene y devuelva falso si no existe.

Shreevardhan
fuente