You are on page 1of 11

Algoritmi per la generazione di permutazioni

Vincenzo La Spesa

4 marzo 2010

v1.2

Indice
1 Algoritmo iterativo per generare permutazioni in ordine lessicografico 2

2 Algoritmo ricorsivo per generare permutazioni 3

3 Algoritmo degli scambi semplici (Plain changes - Johnson-Trotter) 4

4 Determinare una specifica permutazione dall’insieme delle permutazioni 6


4.1 I Factoradic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
4.2 Interpretazione dei codici di Lehmer . . . . . . . . . . . . . . . . . . . . . 6
4.2.1 Algoritmi per la generazione di un Factoradic . . . . . . . . . . . . 8
4.3 Generazione di permutazioni casuali con i factoradic . . . . . . . . . . . . 9

5 Ricavare il numero di una permutazione (ranking) 9

6 Generare permutazioni casuali (algoritmo di Fisher-Yates o Knuth shuffle) 10


6.1 Problemi di bias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10

Riferimenti bibliografici 11

http://thedarshan.wordpress.com/

Quest’opera è stata rilasciata sotto la licenza Creative Commons Attribuzione-Non


commerciale-Condividi allo stesso modo 2.5 Italia. Per leggere una copia della li-
cenza visita il sito web http://creativecommons.org/licenses/by-nc-sa/2.5/it/ o spe-
disci una lettera a Creative Commons, 171 Second Street, Suite 300, San Francisco,
California, 94105, USA.

1
1 Algoritmo iterativo per generare permutazioni in ordine lessicografico
L’algoritmo seguente è probabilmente il più semplice algoritmo iterativo per listare le
permutazioni di un insieme di elementi, analizza semplicemente la permutazione attuale
per ricavarne la prossima basandosi sul fatto che le permutazioni devono seguire un
ordinamento lessicografico (nel caso numerico qui analizzato ogni permutazione deve
essere la minima permutazione maggiore di quella corrente)
considerando la permutazione come un vettore di n elementi numerici l’algoritmo
determina la permutazione successiva nel seguente modo:
1. a partire dall’ultimo elemento scorre l’array cercando il primo elemento preceduto
da un elemento minore
2. se non esiste si è giunti all’ultima permutazione possibile e l’algoritmo deve termi-
nare
3. se esiste ricomincia a scorrere il vettore dalla fine cercando il primo elemento
maggiore all’elemento trovato al punto1
4. si invertono gli elementi trovati al punto1 e al punto3
5. se lo scambio non è avvenuto tra elementi di livello minimo (l’ultimo e il penultimo)
si ripristina l’ordinamento interno invertendo le posizioni degli elementi che si
trovano tra l’elemento trovato al punto1 e quello trovato al punto3
l’algoritmo è molto semplice e molto generico ma non è efficiente, infatti il fatto di dover
analizzare a ogni iterazione la struttura attuale del vettore ne incrementa di molto la
complessità.
Listing 1: Algoritmo semplice
public boolean p r o s s i m a ( ) {
i nt i = l e n − 1 ;
while ( i >0 && ( e l e m e n t i [ i − 1 ] >= e l e m e n t i [ i ] ) ) i −−;
i f ( i ==0)return f a l s e ;
i nt j = l e n ;
// ( i −1) è l ’ e l e m e n t o t r o v a t o con i l c i c l o p r e c e d e n t e
while ( e l e m e n t i [ j − 1 ] <= e l e m e n t i [ i − 1 ] ) j −−;
swap ( i − 1 , j − 1 ) ;
i ++;
j = len ;
// i n v e r t o g l i e l e m e n t i t r a i due e l e m e n t i s c a m b i a t i
while ( i < j ) {
swap ( i − 1 , j − 1 ) ;
i ++;
j −−;
}
return true ;
}

2
2 Algoritmo ricorsivo per generare permutazioni
L’uso di un algoritmo ricorsivo per la generazione di permutazioni non è particolarmen-
te conveniente perche non permette di generare una permutazione per volta, e inoltre
l’approccio iterativo è di persè più veloce perche non usa memoria di stack, detto questo
ho deciso di includere comunque questo algoritmo come esempio “didattico”.
L’implementazione ricorsiva è una tipica applicazione dell’approccio divide et impera

Le permutazioni di un vettore di n elementi si ottengono calcolando le permutazioni


del sottovettore di n-1 elementi e poi scambiando l’n-esimo elemento con ogni elemen-
to del sottovettore. Il caso base è la permutazione di un vettore di due elementi che è
semplicemente uno scambio

Listing 2: Algoritmo ricorsivo


s t a t i c void p e r m u t a r i c o r s i v o ( i nt [ ] e l e m e n t i , i nt da , i nt a ) {
i nt k ;
i f ( a > da ) {
for ( k = a ; k >= da ; k−−){
A r r a y u t i l . swap ( e l e m e n t i , k , a ) ;
p e r m u t a r i c o r s i v o ( e l e m e n t i , da , a − 1 );
A r r a y u t i l . swap ( e l e m e n t i , k , a ) ;
}
} e l s e { System . out . p r i n t l n ( A r r a y u t i l . dump ( e l e m e n t i ) ) ; }
}

2,1,3,4 2,1,4,3 4,1,3,2 2,4,3,1


1,3,2,4 1,4,2,3 1,3,4,2 4,3,2,1
3,1,2,4 4,1,2,3 3,1,4,2 3,4,2,1
3,2,1,4 4,2,1,3 3,4,1,2 3,2,4,1
2,3,1,4 2,4,1,3 4,3,1,2 2,3,4,1

Tabella 1: Esecuzione dell’algoritmo su un vettore di 4 elementi

3
3 Algoritmo degli scambi semplici (Plain changes - Johnson-Trotter)
L’algoritmo degli scambi semplici è stato ideato nel diciassettesimo secolo in Inghilterra
da parte dei suonatori di campane che avevano sviluppato il buffo passatempo di suonare
le campane secondo tutte le permutazioni possibili. 1
Le prime traccie scritte di questo algoritmo risalgono al 1653 ed è trattato in maniera
estensiva nel libro Tintinnalogia del 1668 che gli dedica addirittura 60 pagine... Le prime
implementazioni informatiche documentate risalgono invece al 1962 ad opera di H. F.
Trotter e al 1963 con S.M. Johnson ed è per questo che l’algoritmo è noto anche come
l’algoritmo di Johnson-Trotter.
L’algoritmo opera facendo in modo che solo una coppia venga scambiata ad ogni
permutazione e che la coppia sia sempre formata da elementi adiacenti.
L’idea da cui nasce l’algoritmo è che si possano generare le permutazioni di un vettore
di n elementi scegliendo dei sottovettori di n-1 elementi e facendo scorrere l’n-esimo
elemento all’interno dei sottovettori.
Quando l’n-esimo ha percorso l’intero sottovettore, si calcola la prossima permuta-
zione del sottovettore e si fa scorrere l’n-esimo elemento in senso contrario.
Per implementare questa procedura si usa un secondo vettore che contiene le direzioni
di mobilità possibili (il vettore D) che vengono settate a 1 (mobile a destra) o a -1 (mobile
a sinistra).
Un singolo elemento è mobile soltanto se nella direzione della sua mobilità (vettore D)
l’elemento successivo è inferiore dell’elemento corrente (in particolare il primo elemento
non è mai mobile a sinistra e l’ultimo non è mai mobile a destra).
Si usa inoltre un vettore degli scambi (vettore C) il cui n-esimo elemento rappresenta
il numero di elementi minori di n che stanno alla sua destra. Confrontando le variazioni
del vettore C con la direzione di mobilità contenuta nel vettore D si può ottenere lo
scambio da eseguire per arrivare alla permutazione successiva.
a questo punto l’algoritmo agisce in questo modo:

Finchè esistono elementi mobili se ne trova il maggiore e lo si inverte con l’elemen-


to successivo nella direzione di mobilità, poi si inverte la direzione di mobilita di tutti
gli elementi alla sua destra

Figura 1: Esecuzione dell’algoritmo degli scambi semplici su un insieme di 4 elementi

1
altre informazioni sull’algoritmo e sulla sua origine si trovano in
Knuth, Donald (2005), ”Volume 4 Fascicle 2, Generating All Tuples and Permutations”, The Art of
Computer Programming , Addison-Wesley, paragrafo 2.7.1.2, ISBN 0-201-85393-0

4
Listing 3: Implementazione del Plain Changes
/
 elemen t i []= i l v e t t o r e d e g l i elemen t i
 c [ ] = i l v e t t o r e d e g l i scambi
 d []= i l v e t t o r e d e l l e d i r e z i o n i
 n=dimenzione d e l v e t t o r e
 j=s c o r r e i l v e t t o r e d a l l a f i n e a l l ’ i n i z i o
/

public void l i s t a l l ( ) {
i nt n=t h i s . l e n ;
do{
f l a g=true ;
System . out . p r i n t l n ( A r r a y u t i l . dump( e l e m e n t i ) ) ;
j=n−1;
s =0;
do{
q=c [ j ]+d [ j ] ;
i f ( q==1 && j ==0)return ;
i f ( q>=0 && q !=( j +1)){
A r r a y u t i l . swap ( e l e m e n t i , j −c [ j ]+ s , j −q+s ) ;
c [ j ]=q ;
f l a g=f a l s e ;
}else{
i f ( q==j +1) s++;
i f ( q<0 | | q==j +1){
d [ j ]=−d [ j ] ;
j −−;
}
}
} while ( f l a g ) ;
} while ( true ) ;
}

5
4 Determinare una specifica permutazione dall’insieme delle permuta-
zioni
Potrebbe essere necessario determinare una singola permutazione dall’insieme delle per-
mutazioni ordinate e inutile determinarle tutte.
In questo caso si deve evitare di generare tutte le permutazioni fino a quella cercata in-
quanto si vuole determinare una specifica permutazione in tempo costante o quantomeno
proporzionale al numero di elementi del dominio.
La soluzione risiede nell’uso dei factoradic

4.1 I Factoradic
Di solito un numero viene rappresentato come un polinomio di potenze della base

1936 = 1 · 103 + 9 · 102 + 3 · 101 + 6 · 100

inquanto le potenze dei primi n numeri costituiscono una base che permette di rappre-
sentare univocamente un numero con un polinomio.
Anche i fattoriali dei primi n numeri costituiscono una base , quindi è possibile
rappresentare univocamente un numero come polinomio dei fattoriali della base

1936 = 2 · 6! + 4 · 5! + 0 · 4! + 2 · 3! + 2 · 2! + 0 · 1!

questa rappresentazione è detta factoradic. L’univocità di questa rappresentazione è


garantita dal fatto che la somma dei primi n fattoriali moltiplicati per il loro indice è
sempre il fattoriale successivo meno 1.
n

i · i! = (n + 1)! − 1
i=0

caratteristica fondamentale dei factoradic è il costistuire un codice che permette di as-


sociare al numero n rappresentato la n-esima permutazione dell’insieme degli elementi
della base, questo “codice di traduzione” viene chiamato codice di Lehmer

4.2 Interpretazione dei codici di Lehmer, generazione della permutazione associata


Dal factoradic si può ottenere la permutazione invertendo i singoli elementi o i segmenti
della permutazione di base [1,2,3,4] con gli elementi a destra usando l’elemento del codice
di Lehmer come lunghezza del segmento da spostare.
Per esempio

 il codice 0110 indica che, partendo dalla permutazione di base, si deve invertire
l’elemento 2 e poi l’elemento 3 ottenendo la permutazione [1,3,4,2]

 il codice 0200 indica di invertire l’elemento 2 e il successivo ( il numero due indica


questo: due elementi di uno e NON un elemento di due) ottenendo [1,4,2,3]

6
n Factoradic Permutazione Permutazione Vettore degli Vettore delle
(lessicografica) (plain changes) scambi direzioni

0 0000 [ 1,2,3,4 ] [ 1,2,3,4 ] [ 0,0,0,0 ] ----


1 0010 [ 1,2,4,3 ] [ 1,2,4,3 ] [ 0,0,0,1 ] ----
2 0100 [ 1,3,2,4 ] [ 1,4,2,3 ] [ 0,0,0,2 ] ----
3 0110 [ 1,3,4,2 ] [ 4,1,2,3 ] [ 0,0,0,3 ] ----
4 0200 [ 1,4,2,3 ] [ 4,1,3,2 ] [ 0,0,1,3 ] +---
5 0210 [ 1,4,3,2 ] [ 1,4,3,2 ] [ 0,0,1,2 ] -+--
6 1000 [ 2,1,3,4 ] [ 1,3,4,2 ] [ 0,0,1,1 ] --+-
7 1010 [ 2,1,4,3 ] [ 1,3,2,4 ] [ 0,0,1,0 ] ---+
8 1100 [ 2,3,1,4 ] [ 3,1,2,4 ] [ 0,0,2,0 ] ----
9 1110 [ 2,3,4,1 ] [ 3,1,4,2 ] [ 0,0,2,1 ] ----
10 1200 [ 2,4,1,3 ] [ 3,4,1,2 ] [ 0,0,2,2 ] ----
11 1210 [ 2,4,3,1 ] [ 4,3,1,2 ] [ 0,0,2,3 ] ----
12 2000 [ 3,1,2,4 ] [ 4,3,2,1 ] [ 0,1,2,3 ] ++--
13 2010 [ 3,1,4,2 ] [ 3,4,2,1 ] [ 0,1,2,2 ] ++--
14 2100 [ 3,2,1,4 ] [ 3,2,4,1 ] [ 0,1,2,1 ] +-+-
15 2110 [ 3,2,4,1 ] [ 3,2,1,4 ] [ 0,1,2,0 ] +--+
16 2200 [ 3,4,1,2 ] [ 2,3,1,4 ] [ 0,1,1,0 ] -+--
17 2210 [ 3,4,2,1 ] [ 2,3,4,1 ] [ 0,1,1,1 ] -+--
18 3000 [ 4,1,2,3 ] [ 2,4,3,1 ] [ 0,1,1,2 ] --+-
19 3010 [ 4,1,3,2 ] [ 4,2,3,1 ] [ 0,1,1,3 ] --+-
20 3100 [ 4,2,1,3 ] [ 4,2,1,3 ] [ 0,1,0,3 ] +--+
21 3110 [ 4,2,3,1 ] [ 2,4,1,3 ] [ 0,1,0,2 ] -+-+
22 3200 [ 4,3,1,2 ] [ 2,1,4,3 ] [ 0,1,0,1 ] --++
23 3210 [ 4,3,2,1 ] [ 2,1,3,4 ] [ 0,1,0,0 ] --++

Tabella 2: Permutazioni di un insieme di 4 elementi secondo i vari algoritmi descritti

7
4.2.1 Algoritmi per la generazione di un Factoradic e della permutazione associata
l’algoritmo di generazione è molto semplice: le singole componenti vengono calcolate con
l’operatore modulo
E’ lo stesso tipo di algoritmo che si usa per convertire un numero decimale in una base
diversa, si va dividendo il numero per la base e i resti delle divisioni vanno a costituire il
numero nella base di arrivo, solo che in questo caso la base non è costante ma decresce.

Listing 4: Algoritmo per la generazione di un Factoradic


public s t a t i c i nt [ ] f a c t o r a d i c ( i nt numero , i nt ba se ) {
a s s e r t ( base >1 && numero >= 0 ) ;
i nt [ ] f = new i nt [ ba se ] ;
for ( i nt j = 1 ; j <= ba se ; ++j )
{
f [ base−j ] = numero % j ;
numero /= j ;
}
return f ;
}

Listing 5: Dal Factoradic alla permutazione


public i nt [ ] per muta zio ne ( i nt n ) {
i nt [ ] lehmer=MyMath . f a c t o r a d i c ( n , l e n ) ;
i nt a ;
// c r e o l a p e r m u t a z i o n e d i b a s e
for ( a = 0 ; a < l e n ; a++)e l e m e n t i [ a ] = a + 1 ;
// a p p l i c o i l c o d i c e
for ( a = 0 ; a < l e n ; a++){
i f ( lehmer [ a]==1)
// caso b a n a l e , mi l i m i t o a f a r e uno swap
swap ( a , a + 1 );
e l s e i f ( lehmer [ a ] >1){
// caso g e n e r i c o , s p o s t o un segmento
i nt k=lehmer [ a]+ a ;
i nt b u f f e r =e l e m e n t i [ k ] ;
while ( k>a ) {
swap ( k−1 ,k ) ;
k−−;
}
e l e m e n t i [ a ]= b u f f e r ;
}
}
return e l e m e n t i ;
}

8
4.3 Generazione di permutazioni casuali con i factoradic
Dando come input alla funzione precedente un numero casuale compreso nel numero di
permutazioni si ottiene ovviamente una permutazione casuale.

5 Ricavare il numero di una permutazione (ranking)


I singoli termini del codice di Lehmer si possono anche interpretare come il numero di
elementi che si trovano “al posto sbagliato”, cioè il numero di elementi minori dell’ele-
mento preso in esame che si trovano alla sua destra (e che quindi violano l’ordinamento
crescente degli elementi).
Applicando questo teorema è possibile ricavare il codice di Lehmer di una permuta-
zione e dal codice ricavare la posizione della permutazione.
I singoli elementi del codice di Lehmer sono costituiti dal numero di elementi minori
dell’elemento che si trovano alla sua destra

Listing 6: Generazione del codice di Lehmer


public s t a t i c i nt [ ] i n v e r s e L e h m e r ( i nt [ ] v ) {
i nt n=v . l e n g t h ;
i nt [ ] l h=new i nt [ n ] ;
l h [ n−1]=0;
for ( i nt a =0; a<n−1; ++a ) {
i nt i = 0 ;
for ( i nt b=a ; b<n ; ++b )
i f ( v [ b]<v [ a ] ) i ++;
lh [ a ] = i ;
}
return l h ;
}

Listing 7: Calcolo della posizione della permutazione


public s t a t i c i nt rank ( i nt [ ] per muta zio ne ) {
i nt [ ] l=i n v e r s e L e h m e r ( per muta zio ne ) ;
i nt f =1;
i nt r =0;
i nt n=per muta zio ne . l e n g t h ;
for ( i nt a =1;a<(n + 1 ); a++){
r=r+f  l [ n−a ] ;
f=f  a ;
}
return r ;
}

9
6 Generare permutazioni casuali (algoritmo di Fisher-Yates o Knuth
shuffle)
Originariamente sviluppato come algoritmo da eseguire “a mano” da Fisher e Yates nel
1938 venne adattato all’uso automatico da Richard Durstenfeld nel 1964 e reso famoso
da Donald E. Knuth anche se probabilmente questi ultimi lo ricrearono da zero vista la
sua semplicità.
L’algoritmo opera semplicemente scorrendo il vettore e scambiando gli elementi con
un altro elemento il cui indice è scelto casualmente. Se la scelta dei numeri casuali è
corretta ogni permutazione ha la stessa probabilità di essere generata.

Listing 8: Shuffling
public s t a t i c i nt [ ] s h u f f l e ( i nt [ ] e l e m e n t i ) {
Random r = new Random ( ) ;
i nt k , n ;
for ( n = e l e m e n t i . l e n g t h −1; n >= 0 ; n−−)
{
k = r . n e x t I n t ( n + 1 );
A r r a y u t i l . swap ( e l e m e n t i , k , n ) ;
}
return e l e m e n t i ;
}

6.1 Problemi di bias


I modo più comune di provocare una scelta errata dei numeri casuali è l’uso dell’operatore
modulo, per esempio in C si potrebbe ingenuamente generare il numero casuale con
k = rand() % (n+1); questa implementazione non genera bias apprezabili per numeri
piccoli di n ma potrebbe generarne per numeri di n paragonabili all massimo numero
casuale generabile (definito in C come RAND_MAX).
Per capire perche il modulo può provocare bias supponiamo di dover generare un
numero casuale minore di 16 disponendo di un generatore di numeri casuali interi da 0 a
99. generando il numero come k = rand() % 16; l’operatore modulo divide il numero
generato dalla funzione rand() per 17 e ne restituisce il resto.
100 e 16 non sono divisibili, 100
16 = 6.25 il primo numero divisibile per 100 successivo
al 16 è il 20, quindi i numeri da 0 a 3 avranno più possibilità di essere generati in
1
particolare il bias di questi numeri sarà 100/17 = 0.17 cioè il 17%
Kerninghan e Ritchie nel loro The C Programming Language suggeriscono di definire
la macro

#define frand() ((double) rand() / (RAND_MAX+1.0))

ma nemmeno questo metodo è perfetto ,anche se la generazione attraverso i float genera


un bias minore e non distribuito tra i primi numeri un certo bias è comunque presente.

10
L’unico modo per evitare il bias è adattare generatore a generare numeri nel range
corretto, questa soluzione è implementata in Java che usa il generatore di Lehmer.
La descrizione dei generatori di numeri casuali in ogni caso non è lo scopo di questo
articolo, per ulteriori approfondimenti sul generatore di Lehmer rimando al solito testo
di Knuth.

Riferimenti bibliografici
[1] Donald E. Knuth The Art of Computer Programming, Volume 2: Seminumerical
Algorithms, section 3.2.1

[2] Donald E. Knuth (2005), The Art of Computer Programming, Volume 4 Fascicle 2,
Generating All Tuples and Permutations ,Addison-Wesley, paragrafo 2.7.1.2, ISBN
0-201-85393-0

[3] http://thedarshan.wordpress.com/2009/06/30/un-semplice-algoritmo-iterativo-per-
listare-le-permutazioni-di-un-insieme-di-elementi/

[4] http://thedarshan.wordpress.com/2010/02/18/algoritmo-degli-scambi-semplici-
plain-changes-johnson-trotter/

[5] http://thedarshan.wordpress.com/2009/09/19/determinare-una-specifica-
permutazione-dallinsieme-delle-permutazioni/

[6] http://thedarshan.wordpress.com/2009/09/24/ricavare-il-numero-di-una-
permutazione-ranking/

[7] http://thedarshan.wordpress.com/2009/06/30/un-semplice-algoritmo-iterativo-per-
listare-le-permutazioni-di-un-insieme-di-elementi/

11