Para descrever uma permutação de n elementos, você vê que para a posição em que o primeiro elemento termina, você tem n possibilidades, portanto, pode descrever isso com um número entre 0 e n-1. Para a posição em que o próximo elemento termina, você tem n-1 possibilidades restantes, então você pode descrever isso com um número entre 0 e n-2.
Et cetera até que você tenha n números.
Como exemplo para n = 5, considere a permutação que traz abcde
para caebd
.
a
, o primeiro elemento, termina na segunda posição, então atribuímos a ele o índice 1 .
b
termina na quarta posição, que seria o índice 3, mas é o terceiro restante, então o atribuímos 2 .
c
termina na primeira posição restante, que é sempre 0 .
d
termina na última posição restante, que (de apenas duas posições restantes) é 1 .
e
termina na única posição restante, indexada em 0 .
Portanto, temos a sequência de índice {1, 2, 0, 1, 0} .
Agora você sabe que, por exemplo, em um número binário, 'xyz' significa z + 2y + 4x. Para um número decimal,
é z + 10y + 100x. Cada dígito é multiplicado por algum peso e os resultados são somados. O padrão óbvio do peso é, claro, que o peso é w = b ^ k, com b a base do número ek o índice do dígito. (Sempre contarei os dígitos da direita e começando no índice 0 para o dígito mais à direita. Da mesma forma, quando falo sobre o 'primeiro' dígito, quero dizer o mais à direita.)
A razão pela qual os pesos dos dígitos seguem esse padrão é que o número mais alto que pode ser representado pelos dígitos de 0 a k deve ser exatamente 1 menor do que o número mais baixo que pode ser representado usando apenas o dígito k + 1. Em binário, 0111 deve ser menor que 1000. Em decimal, 099999 deve ser menor que 100000.
Codificação para base variável
O espaçamento entre os números subsequentes sendo exatamente 1 é a regra importante. Percebendo isso, podemos representar nossa sequência de índice por um número de base variável . A base para cada dígito é a quantidade de possibilidades diferentes para aquele dígito. Para decimal, cada dígito tem 10 possibilidades, para nosso sistema o dígito mais à direita teria 1 possibilidade e o mais à esquerda teria n possibilidades. Mas como o dígito mais à direita (o último número em nossa sequência) é sempre 0, nós o deixamos de fora. Isso significa que ficamos com as bases 2 a n. Em geral, o k'ésimo dígito terá base b [k] = k + 2. O maior valor permitido para o dígito k é h [k] = b [k] - 1 = k + 1.
Nossa regra sobre os pesos w [k] dos dígitos requer que a soma de h [i] * w [i], onde i vai de i = 0 a i = k, seja igual a 1 * w [k + 1]. Declarado de forma recorrente, w [k + 1] = w [k] + h [k] * w [k] = w [k] * (h [k] + 1). O primeiro peso w [0] deve ser sempre 1. A partir daí, temos os seguintes valores:
k h[k] w[k]
0 1 1
1 2 2
2 3 6
3 4 24
... ... ...
n-1 n n!
(A relação geral w [k-1] = k! É facilmente provada por indução.)
O número que obtemos ao converter nossa sequência será então a soma de s [k] * w [k], com k variando de 0 a n-1. Aqui s [k] é o elemento k'th (mais à direita, começando em 0) da sequência. Como exemplo, tome nosso {1, 2, 0, 1, 0}, com o elemento mais à direita retirado conforme mencionado antes: {1, 2, 0, 1} . Nossa soma é 1 * 1 + 0 * 2 + 2 * 6 + 1 * 24 = 37 .
Observe que se tomarmos a posição máxima para cada índice, teremos {4, 3, 2, 1, 0}, e isso se converte em 119. Uma vez que os pesos em nossa codificação numérica foram escolhidos para que não pulemos quaisquer números, todos os números de 0 a 119 são válidos. Existem precisamente 120 deles, que é n! para n = 5 em nosso exemplo, exatamente o número de permutações diferentes. Portanto, você pode ver nossos números codificados especificando completamente todas as permutações possíveis.
Decodificação de base variável A
decodificação é semelhante à conversão para binário ou decimal. O algoritmo comum é este:
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;
}
Para nosso número de base variável:
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
}
Isso decodifica corretamente nosso 37 de volta para {1, 2, 0, 1} ( sequence
seria {1, 0, 2, 1}
neste exemplo de código, mas tanto faz ... contanto que você indexe adequadamente). Precisamos apenas adicionar 0 na extremidade direita (lembre-se de que o último elemento sempre tem apenas uma possibilidade para sua nova posição) para recuperar nossa sequência original {1, 2, 0, 1, 0}.
Permutando uma lista usando uma sequência de índice
Você pode usar o algoritmo a seguir para permutar uma lista de acordo com uma sequência de índice específica. É um algoritmo O (n²), infelizmente.
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;
}
Representação comum de permutações
Normalmente, você não representaria uma permutação de forma tão não intuitiva como fizemos, mas simplesmente pela posição absoluta de cada elemento após a aplicação da permutação. Nosso exemplo {1, 2, 0, 1, 0} para abcde
to caebd
é normalmente representado por {1, 3, 0, 4, 2}. Cada índice de 0 a 4 (ou em geral, 0 a n-1) ocorre exatamente uma vez nesta representação.
Aplicar uma permutação neste formulário é fácil:
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];
}
Invertê-lo é muito semelhante:
for (int i = 0; i < n; i++)
{
list[i] = permuted[permutation[i]];
}
Convertendo de nossa representação para a representação comum
Observe que se pegarmos nosso algoritmo para permutar uma lista usando nossa sequência de índice e aplicá-lo à permutação de identidade {0, 1, 2, ..., n-1}, obteremos o permutação inversa , representada na forma comum. ( {2, 0, 4, 1, 3} em nosso exemplo).
Para obter a pré-mutação não invertida, aplicamos o algoritmo de permutação que acabei de mostrar:
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 você pode apenas aplicar a permutação diretamente, usando o algoritmo de permutação 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]];
}
Observe que todos os algoritmos para lidar com permutações na forma comum são O (n), enquanto a aplicação de uma permutação em nossa forma é O (n²). Se você precisar aplicar uma permutação várias vezes, primeiro converta-a para a representação comum.