Generare permutazioni pigramente


87

Sto cercando un algoritmo per generare permutazioni di un set in modo tale da poterne fare un pigro elenco in Clojure. cioè mi piacerebbe iterare su un elenco di permutazioni in cui ogni permutazione non viene calcolata fino a quando non la richiedo e tutte le permutazioni non devono essere archiviate in memoria contemporaneamente.

In alternativa sto cercando un algoritmo in cui dato un certo insieme, restituirà la permutazione "successiva" di quell'insieme, in modo tale che chiamando ripetutamente la funzione sul proprio output scorrerà tutte le permutazioni dell'insieme originale, in un certo ordine (quale sia l'ordine non importa).

Esiste un tale algoritmo? La maggior parte degli algoritmi di generazione di permutazioni che ho visto tendono a generarli tutti in una volta (di solito in modo ricorsivo), il che non scala a set molto grandi. Un'implementazione in Clojure (o un altro linguaggio funzionale) sarebbe utile ma posso capirlo dallo pseudocodice.

Risposte:


139

Sì, c'è un algoritmo di "permutazione successiva", ed è troppo abbastanza semplice. La libreria di modelli standard C ++ (STL) ha anche una funzione chiamata next_permutation.

L'algoritmo trova effettivamente la successiva permutazione, la successiva lessicograficamente. L'idea è questa: supponi di ricevere una sequenza, ad esempio "32541". Qual è la prossima permutazione?

Se ci pensi, vedrai che è "34125". E i tuoi pensieri erano probabilmente qualcosa di questo: in "32541",

  • non c'è modo di mantenere fisso il "32" e trovare una permutazione successiva nella parte "541", perché quella permutazione è già l'ultima per 5,4, e 1 - è ordinata in ordine decrescente.
  • Quindi dovrai cambiare il "2" in qualcosa di più grande - in effetti, al numero più piccolo più grande di quello nella parte "541", vale a dire 4.
  • Ora, una volta deciso che la permutazione inizierà come "34", il resto dei numeri dovrebbe essere in ordine crescente, quindi la risposta è "34125".

L'algoritmo consiste nell'implementare precisamente quella linea di ragionamento:

  1. Trova la "coda" più lunga ordinata in ordine decrescente. (La parte "541".)
  2. Cambia il numero appena prima della coda (il "2") con il numero più piccolo più grande di quello nella coda (il 4).
  3. Ordina la coda in ordine crescente.

Puoi fare (1.) in modo efficiente partendo dalla fine e andando indietro finché l'elemento precedente non è più piccolo dell'elemento corrente. Puoi fare (2.) semplicemente scambiando il "4" con il "2", così avrai "34521". Una volta fatto questo, puoi evitare di usare un algoritmo di ordinamento per (3.), perché la coda era, ed è ancora (pensaci), ordinato in ordine decrescente, quindi deve solo essere invertito.

Il codice C ++ fa esattamente questo (guarda i sorgenti nel /usr/include/c++/4.0.0/bits/stl_algo.htuo sistema, o vedi questo articolo ); dovrebbe essere semplice tradurlo nella tua lingua: [Leggi "BidirectionalIterator" come "pointer", se non hai familiarità con gli iteratori C ++. Il codice ritorna falsese non c'è una successiva permutazione, cioè siamo già in ordine decrescente.]

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

Potrebbe sembrare che possa essere necessario O (n) tempo per permutazione, ma se ci pensi più attentamente, puoi dimostrare che ci vuole O (n) tempo per tutte le permutazioni in totale, quindi solo O (1) - tempo costante - per permutazione.

La cosa buona è che l'algoritmo funziona anche quando hai una sequenza con elementi ripetuti: con, diciamo, "232254421", troverebbe la coda come "54421", scambia i "2" e "4" (quindi "232454221" ), inverte il resto, dando "232412245", che è la successiva permutazione.


2
Funzionerà, supponendo che tu abbia un ordine totale sugli elementi.
Chris Conway,

10
Se inizi con un set, puoi definire arbitrariamente un ordine totale sugli elementi; mappare gli elementi a numeri distinti. :-)
ShreevatsaR

3
Questa risposta non ottiene abbastanza voti positivi, ma posso votarla solo una volta ... :-)
Daniel C. Sobral

1
@ Mass: Non esattamente ... più o meno, puoi passare da 1 a un numero maggiore. Usando l'esempio: Inizia con 32541. La coda è 541. Dopo aver eseguito i passaggi necessari, la successiva permutazione è 34125. Ora la coda è solo 5. Incrementando 3412 usando il 5 e scambiando, la successiva permutazione è 34152. Ora la coda è 52, di lunghezza 2. Poi diventa 34215 (lunghezza coda 1), 34251 (lunghezza coda 2), 34512 (lunghezza 1), 34521 (lunghezza 3), 35124 (lunghezza 1), ecc. Hai ragione che la coda è piccolo il più delle volte, motivo per cui l'algoritmo ha buone prestazioni su più chiamate.
ShreevatsaR

1
@SamStoelinga: Hai ragione, in realtà. O (n log n) è O (log n!). Avrei dovuto dire O (n!).
ShreevatsaR

42

Supponendo che stiamo parlando dell'ordine lessicografico sui valori permutati, ci sono due approcci generali che puoi usare:

  1. trasformare una permutazione degli elementi nella successiva permutazione (come postato da ShreevatsaR), o
  2. calcola direttamente la nth permutazione, contando nda 0 in su.

Per quelli (come me ;-) che non parlano c ++ come nativi, l'approccio 1 può essere implementato dal seguente pseudo-codice, assumendo l'indicizzazione a base zero di un array con indice zero a "sinistra" (sostituendo qualche altra struttura , come una lista, è "lasciato come esercizio" ;-):

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

Ecco un esempio che inizia con una permutazione corrente di 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

Per il secondo approccio (calcolo diretto della nth permutazione), ricorda che ci sono N!permutazioni di Nelementi. Pertanto, se stai permutando Nelementi, le prime (N-1)!permutazioni devono iniziare con l'elemento più piccolo, le successive (N-1)!permutazioni devono iniziare con il secondo più piccolo e così via. Questo porta al seguente approccio ricorsivo (sempre in pseudo-codice, numerando le permutazioni e le posizioni da 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

Quindi, ad esempio, la 13a permutazione di ABCD si trova come segue:

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

Per inciso, la "rimozione" di elementi può essere rappresentata da un array parallelo di booleani che indica quali elementi sono ancora disponibili, quindi non è necessario creare un nuovo array ad ogni chiamata ricorsiva.

Quindi, per iterare attraverso le permutazioni di ABCD, conta da 0 a 23 (4! -1) e calcola direttamente la permutazione corrispondente.


1
++ La tua risposta è sottovalutata. Non per togliere la risposta accettata, ma il secondo approccio è più potente perché può essere generalizzato anche alle combinazioni. Una discussione completa mostrerebbe la funzione inversa da sequenza a indice.
Muori a Sente il

1
Infatti. Sono d'accordo con il commento precedente - anche se la mia risposta fa un numero leggermente inferiore di operazioni per la domanda specifica posta, questo approccio è più generale, poiché funziona, ad esempio, per trovare la permutazione a K passi da una data.
ShreevatsaR

4

Dovresti controllare l' articolo sulle permutazioni su wikipeda. Inoltre, c'è il concetto di numeri fattoriali .

Ad ogni modo, il problema matematico è piuttosto difficile.

In C#è possibile utilizzare un iteratore interrompere l'algoritmo di permutazione utilizzando yield. Il problema con questo è che non puoi andare avanti e indietro o usare un file index.


5
"Comunque, il problema matematico è piuttosto difficile." No, non lo è :-)
ShreevatsaR

Beh, lo è .. se non conosci i numeri Factoradic non c'è modo di trovare un algoritmo appropriato in un tempo accettabile. È come cercare di risolvere un'equazione di 4 ° grado senza conoscere il metodo.
Bogdan Maxim,

1
Oh scusa, pensavo stessi parlando del problema originale. Comunque non vedo ancora perché hai bisogno di "numeri fattoriali" ... è abbastanza semplice assegnare un numero a ciascuno degli n! permutazioni di un dato insieme e costruire una permutazione da un numero. [Solo un po 'di programmazione / conteggio dinamico ..]
ShreevatsaR

1
In idiomatico C #, un iteratore viene più correttamente definito enumeratore .
Drew Noakes

@ ShreevatsaR: Come faresti a non generare tutte le permutazioni? Ad esempio, se è necessario generare l'n! Esima permutazione.
Jacob

3

Altri esempi di algoritmi di permutazione per generarli.

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

  1. Utilizza l'algoritmo di Fike, quello dei più veloci conosciuti.
  2. Utilizza l'algoritmo per l'ordine lexografico.
  3. Utilizza il non flessografico, ma viene eseguito più velocemente del punto 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.


2

la funzione permutazioni in clojure.contrib.lazy_seqs afferma già di fare proprio questo.


Grazie, non ne ero a conoscenza. Sostiene di essere pigro, ma purtroppo funziona molto male e riempie facilmente lo stack.
Brian Carper

La pigrizia può certamente causare overflow dello stack come spiegato, ad esempio, in questa risposta.
crockeea
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.