Pour décrire une permutation de n éléments, vous voyez que pour la position à laquelle le premier élément se termine, vous avez n possibilités, vous pouvez donc le décrire avec un nombre compris entre 0 et n-1. Pour la position à laquelle se termine l'élément suivant, vous avez n-1 possibilités restantes, vous pouvez donc la décrire avec un nombre compris entre 0 et n-2.
Et cetera jusqu'à ce que vous ayez n nombres.
À titre d'exemple pour n = 5, considérons la permutation qui conduit abcde
à caebd
.
a
, le premier élément, se termine à la deuxième position, nous lui attribuons donc l'indice 1 .
b
finit à la quatrième position, qui serait l'indice 3, mais c'est la troisième qui reste, donc nous lui attribuons 2 .
c
se termine à la première position restante, qui est toujours 0 .
d
se termine à la dernière position restante, qui (sur seulement deux positions restantes) est 1 .
e
se retrouve à la seule position restante, indexée à 0 .
Nous avons donc la séquence d'index {1, 2, 0, 1, 0} .
Vous savez maintenant que par exemple dans un nombre binaire, «xyz» signifie z + 2y + 4x. Pour un nombre décimal,
c'est z + 10y + 100x. Chaque chiffre est multiplié par un poids et les résultats sont additionnés. Le modèle évident dans le poids est bien sûr que le poids est w = b ^ k, avec b la base du nombre et k l'indice du chiffre. (Je compterai toujours les chiffres à partir de la droite et en commençant à l'index 0 pour le chiffre le plus à droite. De même, quand je parle du «premier» chiffre, je veux dire le plus à droite.)
La raison pour laquelle les pondérations des chiffres suivent ce modèle est que le nombre le plus élevé qui peut être représenté par les chiffres de 0 à k doit être exactement 1 inférieur au nombre le plus bas qui peut être représenté en utilisant uniquement le chiffre k + 1. En binaire, 0111 doit être un inférieur à 1000. En décimal, 099999 doit être un inférieur à 100000.
Encodage en base variable
L'espacement entre les nombres suivants étant exactement de 1 est la règle importante. En réalisant cela, nous pouvons représenter notre séquence d'index par un nombre à base variable . La base de chaque chiffre correspond au nombre de possibilités différentes pour ce chiffre. Pour la décimale, chaque chiffre a 10 possibilités, pour notre système le chiffre le plus à droite aurait 1 possibilité et le plus à gauche aurait n possibilités. Mais comme le chiffre le plus à droite (le dernier nombre de notre séquence) est toujours 0, nous l'oublions. Cela signifie qu'il nous reste les bases 2 à n. En général, le kème chiffre aura la base b [k] = k + 2. La valeur la plus élevée autorisée pour le chiffre k est h [k] = b [k] - 1 = k + 1.
Notre règle sur les poids w [k] des chiffres exige que la somme de h [i] * w [i], où i va de i = 0 à i = k, soit égale à 1 * w [k + 1]. En termes récurrents, w [k + 1] = w [k] + h [k] * w [k] = w [k] * (h [k] + 1). Le premier poids w [0] doit toujours être 1. À partir de là, nous avons les valeurs suivantes:
k h[k] w[k]
0 1 1
1 2 2
2 3 6
3 4 24
... ... ...
n-1 n n!
(La relation générale w [k-1] = k! Est facilement prouvée par récurrence.)
Le nombre que nous obtenons en convertissant notre séquence sera alors la somme de s [k] * w [k], avec k allant de 0 à n-1. Ici, s [k] est le k'e élément (le plus à droite, commençant à 0) de la séquence. À titre d'exemple, prenons notre {1, 2, 0, 1, 0}, avec l'élément le plus à droite retiré comme mentionné précédemment: {1, 2, 0, 1} . Notre somme est 1 * 1 + 0 * 2 + 2 * 6 + 1 * 24 = 37 .
Notez que si nous prenons la position maximale pour chaque index, nous aurions {4, 3, 2, 1, 0}, et cela se convertit en 119. Puisque les poids de notre encodage numérique ont été choisis de manière à ne pas sauter tous les nombres, tous les nombres de 0 à 119 sont valides. Il y en a précisément 120, soit n! pour n = 5 dans notre exemple, précisément le nombre de permutations différentes. Ainsi, vous pouvez voir nos nombres encodés spécifier complètement toutes les permutations possibles.
Décodage à partir de la base de variables Le
décodage est similaire à la conversion en binaire ou décimal. L'algorithme commun est le suivant:
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;
}
Pour notre numéro à base variable:
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
}
Cela décode correctement nos 37 en {1, 2, 0, 1} (ce sequence
serait {1, 0, 2, 1}
dans cet exemple de code, mais peu importe ... tant que vous indexez correctement). Il suffit d'ajouter 0 à l'extrémité droite (rappelez-vous que le dernier élément n'a toujours qu'une seule possibilité pour sa nouvelle position) pour récupérer notre séquence d'origine {1, 2, 0, 1, 0}.
Permutation d'une liste à l'aide d'une séquence d'index
Vous pouvez utiliser l'algorithme ci-dessous pour permuter une liste en fonction d'une séquence d'index spécifique. C'est un algorithme O (n²), malheureusement.
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;
}
Représentation commune des permutations
Normalement, vous ne représenteriez pas une permutation de manière aussi imprudente que nous l'avons fait, mais simplement par la position absolue de chaque élément après l'application de la permutation. Notre exemple {1, 2, 0, 1, 0} pour abcde
to caebd
est normalement représenté par {1, 3, 0, 4, 2}. Chaque indice de 0 à 4 (ou en général de 0 à n-1) apparaît exactement une fois dans cette représentation.
L'application d'une permutation sous cette forme est 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'inverser est très similaire:
for (int i = 0; i < n; i++)
{
list[i] = permuted[permutation[i]];
}
Conversion de notre représentation à la représentation commune
Notez que si nous prenons notre algorithme pour permuter une liste en utilisant notre séquence d'index, et l'appliquons à la permutation d'identité {0, 1, 2, ..., n-1}, nous obtenons le permutation inverse , représentée sous la forme courante. ( {2, 0, 4, 1, 3} dans notre exemple).
Pour obtenir la prémutation non inversée, nous appliquons l'algorithme de permutation que je viens de montrer:
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];
}
Ou vous pouvez simplement appliquer la permutation directement, en utilisant l'algorithme de permutation inverse:
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]];
}
Notez que tous les algorithmes pour traiter les permutations sous la forme courante sont O (n), tandis que l'application d'une permutation sous notre forme est O (n²). Si vous devez appliquer une permutation plusieurs fois, convertissez-la d'abord en représentation commune.