Per descrivere una permutazione di n elementi, vedi che per la posizione in cui finisce il primo elemento, hai n possibilità, quindi puoi descriverlo con un numero compreso tra 0 e n-1. Per la posizione in cui finisce l'elemento successivo, hai n-1 possibilità rimanenti, quindi puoi descriverlo con un numero compreso tra 0 e n-2.
Eccetera finché non hai n numeri.
Come esempio per n = 5, considera la permutazione che porta abcde
a caebd
.
a
, il primo elemento, finisce alla seconda posizione, quindi gli assegniamo indice 1 .
b
finisce alla quarta posizione, che sarebbe indice 3, ma è la terza rimasta, quindi gli assegniamo 2 .
c
finisce alla prima posizione rimanente, che è sempre 0 .
d
finisce nell'ultima posizione rimasta, che (su due sole posizioni rimanenti) è 1 .
e
finisce nell'unica posizione rimasta, indicizzata a 0 .
Quindi abbiamo la sequenza indice {1, 2, 0, 1, 0} .
Ora sai che, ad esempio, in un numero binario, "xyz" significa z + 2y + 4x. Per un numero decimale,
è z + 10y + 100x. Ogni cifra viene moltiplicata per un certo peso e i risultati vengono sommati. Il modello ovvio nel peso è ovviamente che il peso è w = b ^ k, con b la base del numero e k l'indice della cifra. (Conterò sempre le cifre da destra e partendo dall'indice 0 per la cifra più a destra. Allo stesso modo quando parlo della "prima" cifra intendo la cifra più a destra.)
Il motivo per cui i pesi delle cifre seguono questo schema è che il numero più alto che può essere rappresentato dalle cifre da 0 a k deve essere esattamente 1 inferiore al numero più basso che può essere rappresentato utilizzando solo la cifra k + 1. In binario, 0111 deve essere uno inferiore a 1000. In decimale, 099999 deve essere uno inferiore a 100000.
Codifica in base variabile
La spaziatura tra i numeri successivi che è esattamente 1 è la regola importante. Rendendoci conto, possiamo rappresentare la nostra sequenza indice con un numero a base variabile . La base per ogni cifra è la quantità di diverse possibilità per quella cifra. Per i decimali ogni cifra ha 10 possibilità, per il nostro sistema la cifra più a destra avrebbe 1 possibilità e quella più a sinistra avrà n possibilità. Ma poiché la cifra più a destra (l'ultimo numero nella nostra sequenza) è sempre 0, la tralasciamo. Ciò significa che ci rimangono le basi da 2 a n. In generale, la k-esima cifra avrà base b [k] = k + 2. Il valore più alto consentito per la cifra k è h [k] = b [k] - 1 = k + 1.
La nostra regola sui pesi w [k] delle cifre richiede che la somma di h [i] * w [i], dove i va da i = 0 a i = k, sia uguale a 1 * w [k + 1]. Dichiarato in modo ricorrente, w [k + 1] = w [k] + h [k] * w [k] = w [k] * (h [k] + 1). Il primo peso w [0] dovrebbe sempre essere 1. Partendo da lì, abbiamo i seguenti valori:
k h[k] w[k]
0 1 1
1 2 2
2 3 6
3 4 24
... ... ...
n-1 n n!
(La relazione generale w [k-1] = k! È facilmente dimostrata per induzione.)
Il numero che otteniamo convertendo la nostra sequenza sarà quindi la somma di s [k] * w [k], con k che va da 0 a n-1. Qui s [k] è l'elemento k-esimo (più a destra, a partire da 0) della sequenza. Ad esempio, prendi il nostro {1, 2, 0, 1, 0}, con l'elemento più a destra rimosso come menzionato prima: {1, 2, 0, 1} . La nostra somma è 1 * 1 + 0 * 2 + 2 * 6 + 1 * 24 = 37 .
Nota che se prendiamo la posizione massima per ogni indice, avremmo {4, 3, 2, 1, 0} e questo viene convertito in 119. Poiché i pesi nella nostra codifica numerica sono stati scelti in modo da non saltare qualsiasi numero, tutti i numeri da 0 a 119 sono validi. Ce ne sono esattamente 120, ovvero n! per n = 5 nel nostro esempio, precisamente il numero di diverse permutazioni. Quindi puoi vedere i nostri numeri codificati che specificano completamente tutte le possibili permutazioni.
La decodifica dalla decodifica a base variabile
è simile alla conversione in binario o decimale. L'algoritmo comune è questo:
int number = 42;
int base = 2;
int[] bits = new int[n];
for (int k = 0; k < bits.Length; k++)
{
bits[k] = number % base;
number = number / base;
}
Per il nostro numero a base variabile:
int n = 5;
int number = 37;
int[] sequence = new int[n - 1];
int base = 2;
for (int k = 0; k < sequence.Length; k++)
{
sequence[k] = number % base;
number = number / base;
base++; // b[k+1] = b[k] + 1
}
Questo decodifica correttamente il nostro 37 di nuovo a {1, 2, 0, 1} ( sequence
sarebbe {1, 0, 2, 1}
in questo esempio di codice, ma qualunque cosa ... purché indicizzi in modo appropriato). Dobbiamo solo aggiungere 0 all'estremità destra (ricorda che l'ultimo elemento ha sempre una sola possibilità per la sua nuova posizione) per recuperare la nostra sequenza originale {1, 2, 0, 1, 0}.
Permutare un elenco utilizzando una sequenza indice
È possibile utilizzare l'algoritmo seguente per permutare un elenco in base a una sequenza indice specifica. Purtroppo è un algoritmo O (n²).
int n = 5;
int[] sequence = new int[] { 1, 2, 0, 1, 0 };
char[] list = new char[] { 'a', 'b', 'c', 'd', 'e' };
char[] permuted = new char[n];
bool[] set = new bool[n];
for (int i = 0; i < n; i++)
{
int s = sequence[i];
int remainingPosition = 0;
int index;
// Find the s'th position in the permuted list that has not been set yet.
for (index = 0; index < n; index++)
{
if (!set[index])
{
if (remainingPosition == s)
break;
remainingPosition++;
}
}
permuted[index] = list[i];
set[index] = true;
}
Rappresentazione comune delle permutazioni
Normalmente non rappresenteresti una permutazione in modo non intuitivo come abbiamo fatto, ma semplicemente dalla posizione assoluta di ogni elemento dopo che la permutazione è stata applicata. Il nostro esempio {1, 2, 0, 1, 0} per abcde
to caebd
è normalmente rappresentato da {1, 3, 0, 4, 2}. Ogni indice da 0 a 4 (o in generale da 0 a n-1) si verifica esattamente una volta in questa rappresentazione.
Applicare una permutazione in questa forma è facile:
int[] permutation = new int[] { 1, 3, 0, 4, 2 };
char[] list = new char[] { 'a', 'b', 'c', 'd', 'e' };
char[] permuted = new char[n];
for (int i = 0; i < n; i++)
{
permuted[permutation[i]] = list[i];
}
L'inversione è molto simile:
for (int i = 0; i < n; i++)
{
list[i] = permuted[permutation[i]];
}
Conversione dalla nostra rappresentazione alla rappresentazione comune
Nota che se prendiamo il nostro algoritmo per permutare una lista usando la nostra sequenza indice e lo applichiamo alla permutazione di identità {0, 1, 2, ..., n-1}, otteniamo il permutazione inversa , rappresentata nella forma comune. ( {2, 0, 4, 1, 3} nel nostro esempio).
Per ottenere la premutazione non invertita, applichiamo l'algoritmo di permutazione che ho appena mostrato:
int[] identity = new int[] { 0, 1, 2, 3, 4 };
int[] inverted = { 2, 0, 4, 1, 3 };
int[] normal = new int[n];
for (int i = 0; i < n; i++)
{
normal[identity[i]] = list[i];
}
Oppure puoi semplicemente applicare la permutazione direttamente, utilizzando l'algoritmo di permutazione inversa:
char[] list = new char[] { 'a', 'b', 'c', 'd', 'e' };
char[] permuted = new char[n];
int[] inverted = { 2, 0, 4, 1, 3 };
for (int i = 0; i < n; i++)
{
permuted[i] = list[inverted[i]];
}
Nota che tutti gli algoritmi per trattare le permutazioni nella forma comune sono O (n), mentre l'applicazione di una permutazione nella nostra forma è O (n²). Se è necessario applicare una permutazione più volte, convertirla prima nella rappresentazione comune.