Generando permutaciones perezosamente

87

Estoy buscando un algoritmo para generar permutaciones de un conjunto de tal manera que pueda hacer una lista perezosa de ellos en Clojure. es decir, me gustaría iterar sobre una lista de permutaciones donde cada permutación no se calcula hasta que la solicito, y todas las permutaciones no tienen que almacenarse en la memoria a la vez.

Alternativamente, estoy buscando un algoritmo donde dado un determinado conjunto, devolverá la "siguiente" permutación de ese conjunto, de tal manera que llamar repetidamente a la función en su propia salida recorrerá todas las permutaciones del conjunto original, algún orden (no importa cuál sea el orden).

¿Existe tal algoritmo? La mayoría de los algoritmos de generación de permutación que he visto tienden a generarlos todos a la vez (generalmente de forma recursiva), lo que no escala a conjuntos muy grandes. Una implementación en Clojure (u otro lenguaje funcional) sería útil, pero puedo averiguarlo a partir de un pseudocódigo.

Brian Carper
fuente

Respuestas:

139

Sí, no es un algoritmo "al lado de permutación", y es bastante simple también. La biblioteca de plantillas estándar de C ++ (STL) incluso tiene una función llamada next_permutation.

El algoritmo en realidad encuentra la siguiente permutación: la siguiente lexicográficamente. La idea es la siguiente: suponga que le dan una secuencia, digamos "32541". ¿Cuál es la siguiente permutación?

Si lo piensa, verá que es "34125". Y tus pensamientos probablemente fueron algo así: En "32541",

  • no hay forma de mantener el "32" fijo y encontrar una permutación posterior en la parte "541", porque esa permutación ya es la última para 5, 4 y 1 - está ordenada en orden decreciente.
  • Así que tendrás que cambiar el "2" por algo más grande, de hecho, al número más pequeño más grande que él en la parte "541", es decir, 4.
  • Ahora, una vez que haya decidido que la permutación comenzará como "34", el resto de los números deben estar en orden creciente, por lo que la respuesta es "34125".

El algoritmo consiste en implementar precisamente esa línea de razonamiento:

  1. Encuentre la "cola" más larga que esté ordenada en orden decreciente. (La parte "541").
  2. Cambie el número justo antes de la cola (el "2") al número más pequeño más grande que este en la cola (el 4).
  3. Clasifica la cola en orden creciente.

Puede hacer (1.) de manera eficiente comenzando por el final y yendo hacia atrás siempre que el elemento anterior no sea más pequeño que el elemento actual. Puede hacer (2.) simplemente intercambiando el "4" con el "2", por lo que tendrá "34521". Una vez que haga esto, puede evitar usar un algoritmo de clasificación para (3.), porque la cola estaba, y todavía está (piense en esto), ordenado en orden decreciente, por lo que solo necesita revertirse.

El código C ++ hace precisamente esto (mire la fuente en /usr/include/c++/4.0.0/bits/stl_algo.hsu sistema, o vea este artículo ); debería ser sencillo traducirlo a su idioma: [Lea "BidirectionalIterator" como "puntero", si no está familiarizado con los iteradores de C ++. El código regresa falsesi no hay siguiente permutación, es decir, ya estamos en orden decreciente.]

template <class BidirectionalIterator>
bool next_permutation(BidirectionalIterator first,
                      BidirectionalIterator last) {
    if (first == last) return false;
    BidirectionalIterator i = first;
    ++i;
    if (i == last) return false;
    i = last;
    --i;
    for(;;) {
        BidirectionalIterator ii = i--;
        if (*i <*ii) {
            BidirectionalIterator j = last;
            while (!(*i <*--j));
            iter_swap(i, j);
            reverse(ii, last);
            return true;
        }
        if (i == first) {
            reverse(first, last);
            return false;
        }
    }
}

Puede parecer que puede tomar O (n) tiempo por permutación, pero si lo piensa más detenidamente, puede probar que toma O (n) tiempo para todas las permutaciones en total, por lo que solo O (1) - tiempo constante - por permutación.

Lo bueno es que el algoritmo funciona incluso cuando tienes una secuencia con elementos repetidos: con, digamos, "232254421", encontraría la cola como "54421", intercambia el "2" y el "4" (entonces "232454221" ), invierta el resto, dando "232412245", que es la siguiente permutación.

ShreevatsaR
fuente
2
Esto funcionará, asumiendo que tiene un orden total en los elementos.
Chris Conway
10
Si comienza con un conjunto, puede definir arbitrariamente un orden total en los elementos; mapear los elementos a números distintos. :-)
ShreevatsaR
3
Esta respuesta simplemente no recibe suficientes votos a favor, pero solo puedo votar una vez ... :-)
Daniel C. Sobral
1
@Masse: No exactamente ... aproximadamente, puedes pasar de 1 a un número mayor. Usando el ejemplo: Comience con 32541. La cola es 541. Después de hacer los pasos necesarios, la siguiente permutación es 34125. Ahora la cola es solo 5. Incrementando 3412 usando el 5 y cambiando, la siguiente permutación es 34152. Ahora la cola es 52, de longitud 2. Luego se convierte en 34215 (longitud de la cola 1), 34251 (longitud de la cola 2), 34512 (longitud 1), 34521 (longitud 3), 35124 (longitud 1), etc. Tiene razón en que la cola es pequeño la mayor parte del tiempo, por lo que el algoritmo tiene un buen rendimiento en múltiples llamadas.
ShreevatsaR
1
@SamStoelinga: De hecho, tienes razón. O (n log n) es O (log n!). Debería haber dicho O (¡n!).
ShreevatsaR
42

Suponiendo que estamos hablando de un orden lexicográfico sobre los valores que se permuta, hay dos enfoques generales que puede utilizar:

  1. transformar una permutación de los elementos a la siguiente permutación (como publicó ShreevatsaR), o
  2. Calcule directamente la npermutación th, mientras cuenta ndesde 0 hacia arriba.

Para aquellos (como yo ;-) que no hablan c ++ como nativos, el enfoque 1 se puede implementar a partir del siguiente pseudocódigo, asumiendo la indexación de base cero de una matriz con índice cero a la "izquierda" (sustituyendo alguna otra estructura , como una lista, se "deja como ejercicio" ;-):

1. scan the array from right-to-left (indices descending from N-1 to 0)
1.1. if the current element is less than its right-hand neighbor,
     call the current element the pivot,
     and stop scanning
1.2. if the left end is reached without finding a pivot,
     reverse the array and return
     (the permutation was the lexicographically last, so its time to start over)
2. scan the array from right-to-left again,
   to find the rightmost element larger than the pivot
   (call that one the successor)
3. swap the pivot and the successor
4. reverse the portion of the array to the right of where the pivot was found
5. return

Aquí hay un ejemplo que comienza con una permutación actual de CADB:

1. scanning from the right finds A as the pivot in position 1
2. scanning again finds B as the successor in position 3
3. swapping pivot and successor gives CBDA
4. reversing everything following position 1 (i.e. positions 2..3) gives CBAD
5. CBAD is the next permutation after CADB

Para el segundo enfoque (cálculo directo de la npermutación-ésima), recuerde que hay N!permutaciones de Nelementos. Por lo tanto, si está permutando Nelementos, las primeras (N-1)!permutaciones deben comenzar con el elemento más pequeño, las siguientes (N-1)!permutaciones deben comenzar con el segundo más pequeño, y así sucesivamente. Esto conduce al siguiente enfoque recursivo (nuevamente en pseudocódigo, numerando las permutaciones y posiciones desde 0):

To find permutation x of array A, where A has N elements:
0. if A has one element, return it
1. set p to ( x / (N-1)! ) mod N
2. the desired permutation will be A[p] followed by
   permutation ( x mod (N-1)! )
   of the elements remaining in A after position p is removed

Entonces, por ejemplo, la permutación 13 de ABCD se encuentra de la siguiente manera:

perm 13 of ABCD: {p = (13 / 3!) mod 4 = (13 / 6) mod 4 = 2; ABCD[2] = C}
C followed by perm 1 of ABD {because 13 mod 3! = 13 mod 6 = 1}
  perm 1 of ABD: {p = (1 / 2!) mod 3 = (1 / 2) mod 2 = 0; ABD[0] = A}
  A followed by perm 1 of BD {because 1 mod 2! = 1 mod 2 = 1}
    perm 1 of BD: {p = (1 / 1!) mod 2 = (1 / 1) mod 2 = 1; BD[1] = D}
    D followed by perm 0 of B {because 1 mod 1! = 1 mod 1 = 0}
      B (because there's only one element)
    DB
  ADB
CADB

Por cierto, la "eliminación" de elementos se puede representar mediante una matriz paralela de valores booleanos que indica qué elementos todavía están disponibles, por lo que no es necesario crear una nueva matriz en cada llamada recursiva.

Entonces, para iterar a través de las permutaciones de ABCD, simplemente cuente de 0 a 23 (4! -1) y calcule directamente la permutación correspondiente.

joel.neely
fuente
1
++ Tu respuesta es subestimada. No para restarle importancia a la respuesta aceptada, pero el segundo enfoque es más poderoso porque también se puede generalizar a combinaciones. Una discusión completa mostraría la función inversa de la secuencia al índice.
Die in Sente
1
En efecto. Estoy de acuerdo con el comentario anterior, aunque mi respuesta realiza un poco menos de operaciones para la pregunta específica formulada, este enfoque es más general, ya que funciona, por ejemplo, para encontrar la permutación que está a K pasos de una determinada.
ShreevatsaR
4

Deberías consultar el artículo de Permutaciones en wikipeda. Además, existe el concepto de números factorádicos .

De todos modos, el problema matemático es bastante difícil.

En C#puede usar an iterator, y detener el algoritmo de permutación usando yield. El problema con esto es que no puede ir y venir, ni usar un archivo index.

Bogdan Maxim
fuente
5
"De todos modos, el problema matemático es bastante difícil". No, no lo es :-)
ShreevatsaR
Bueno, lo es ... si no conoce los números factorádicos, no hay forma de que pueda encontrar un algoritmo adecuado en un tiempo aceptable. Es como intentar resolver una ecuación de cuarto grado sin conocer el método.
Bogdan Maxim
1
Oh, lo siento, pensé que estabas hablando del problema original. Aún no veo por qué necesitas "números factorádicos" de todos modos ... ¡es bastante simple asignar un número a cada uno de los n! permutaciones de un conjunto dado y para construir una permutación a partir de un número. [Solo un poco de programación / conteo dinámico ...]
ShreevatsaR
1
En C # idiomático, un iterador se denomina más correctamente enumerador .
Drew Noakes
@ShreevatsaR: ¿Cómo harías eso sin generar todas las permutaciones? Por ejemplo, si necesita generar la enésima permutación.
Jacob
3

Más ejemplos de algoritmos de permutación para generarlos.

Fuente: http://www.ddj.com/architect/201200326

  1. Utiliza el algoritmo de Fike, que es el más rápido conocido.
  2. Utiliza el Algo para el orden lexográfico.
  3. Utiliza el no flexográfico, pero se ejecuta más rápido que el elemento 2.

1.


PROGRAM TestFikePerm;
CONST marksize = 5;
VAR
    marks : ARRAY [1..marksize] OF INTEGER;
    ii : INTEGER;
    permcount : INTEGER;

PROCEDURE WriteArray;
VAR i : INTEGER;
BEGIN
FOR i := 1 TO marksize
DO Write ;
WriteLn;
permcount := permcount + 1;
END;

PROCEDURE FikePerm ;
{Outputs permutations in nonlexicographic order.  This is Fike.s algorithm}
{ with tuning by J.S. Rohl.  The array marks[1..marksizn] is global.  The   }
{ procedure WriteArray is global and displays the results.  This must be}
{ evoked with FikePerm(2) in the calling procedure.}
VAR
    dn, dk, temp : INTEGER;
BEGIN
IF 
THEN BEGIN { swap the pair }
    WriteArray;
    temp :=marks[marksize];
    FOR dn :=  DOWNTO 1
    DO BEGIN
        marks[marksize] := marks[dn];
        marks [dn] := temp;
        WriteArray;
        marks[dn] := marks[marksize]
        END;
    marks[marksize] := temp;
    END {of bottom level sequence }
ELSE BEGIN
    FikePerm;
    temp := marks[k];
    FOR dk :=  DOWNTO 1
    DO BEGIN
        marks[k] := marks[dk];
        marks[dk][ := temp;
        FikePerm;
        marks[dk] := marks[k];
        END; { of loop on dk }
    marks[k] := temp;l
    END { of sequence for other levels }
END; { of FikePerm procedure }

BEGIN { Main }
FOR ii := 1 TO marksize
DO marks[ii] := ii;
permcount := 0;
WriteLn ;
WrieLn;
FikePerm ; { It always starts with 2 }
WriteLn ;
ReadLn;
END.

2.


PROGRAM TestLexPerms;
CONST marksize = 5;
VAR
    marks : ARRAY [1..marksize] OF INTEGER;
    ii : INTEGER;
    permcount : INTEGER;

PROCEDURE WriteArray; VAR i : INTEGER; BEGIN FOR i := 1 TO marksize DO Write ; permcount := permcount + 1; WriteLn; END;

PROCEDURE LexPerm ; { Outputs permutations in lexicographic order. The array marks is global } { and has n or fewer marks. The procedure WriteArray () is global and } { displays the results. } VAR work : INTEGER: mp, hlen, i : INTEGER; BEGIN IF THEN BEGIN { Swap the pair } work := marks[1]; marks[1] := marks[2]; marks[2] := work; WriteArray ; END ELSE BEGIN FOR mp := DOWNTO 1 DO BEGIN LexPerm<>; hlen := DIV 2; FOR i := 1 TO hlen DO BEGIN { Another swap } work := marks[i]; marks[i] := marks[n - i]; marks[n - i] := work END; work := marks[n]; { More swapping } marks[n[ := marks[mp]; marks[mp] := work; WriteArray; END; LexPerm<> END; END;

BEGIN { Main } FOR ii := 1 TO marksize DO marks[ii] := ii; permcount := 1; { The starting position is permutation } WriteLn < Starting position: >; WriteLn LexPerm ; WriteLn < PermCount is , permcount>; ReadLn; END.

3.


PROGRAM TestAllPerms;
CONST marksize = 5;
VAR
    marks : ARRAY [1..marksize] of INTEGER;
    ii : INTEGER;
    permcount : INTEGER;

PROCEDURE WriteArray; VAR i : INTEGER; BEGIN FOR i := 1 TO marksize DO Write ; WriteLn; permcount := permcount + 1; END;

PROCEDURE AllPerm (n : INTEGER); { Outputs permutations in nonlexicographic order. The array marks is } { global and has n or few marks. The procedure WriteArray is global and } { displays the results. } VAR work : INTEGER; mp, swaptemp : INTEGER; BEGIN IF THEN BEGIN { Swap the pair } work := marks[1]; marks[1] := marks[2]; marks[2] := work; WriteArray; END ELSE BEGIN FOR mp := DOWNTO 1 DO BEGIN ALLPerm<< n - 1>>; IF > THEN swaptemp := 1 ELSE swaptemp := mp; work := marks[n]; marks[n] := marks[swaptemp}; marks[swaptemp} := work; WriteArray; AllPerm< n-1 >; END; END;

BEGIN { Main } FOR ii := 1 TO marksize DO marks[ii] := ii permcount :=1; WriteLn < Starting position; >; WriteLn; Allperm < marksize>; WriteLn < Perm count is , permcount>; ReadLn; END.

Carlos Eduardo Olivieri
fuente
2

la función de permutaciones en clojure.contrib.lazy_seqs ya pretende hacer precisamente esto.


fuente
Gracias, no estaba al tanto. Dice ser vago, pero lamentablemente funciona muy mal y desborda la pila fácilmente.
Brian Carper
La pereza ciertamente puede causar desbordamientos de pila como se explica, por ejemplo, en esta respuesta.
crockeea