为了描述 n 个元素的排列,您会看到对于第一个元素结束的位置,您有 n 个可能性,因此您可以用 0 到 n-1 之间的数字来描述它。对于下一个元素结束的位置,您有 n-1 个剩余可能性,因此您可以用 0 到 n-2 之间的数字来描述它。
等等,直到你有 n 个数字。
作为 n = 5 的示例,考虑带来 的abcde
排列caebd
。
a
,第一个元素,在第二个位置结束,所以我们为其分配索引1。
b
结束在第四个位置,即索引 3,但它是剩下的第三个,所以我们分配它2。
c
最终到达第一个剩余位置,该位置始终为 0。
d
结束在最后一个剩余位置,(仅剩下两个位置)是1。
e
最终到达唯一剩余的位置,索引为0。
所以我们有索引序列{1, 2, 0, 1, 0}。
现在您知道,例如在二进制数中,“xyz”表示 z + 2y + 4x。对于十进制数,
它是 z + 10y + 100x。每个数字乘以某个权重,然后将结果相加。权重中明显的模式当然是权重是 w = b^k,其中 b 是数字的基数,k 是数字的索引。(我总是从右边开始计算数字,并从索引 0 开始计算最右边的数字。同样,当我谈到“第一个”数字时,我指的是最右边的数字。)
数字的权重遵循这种模式的原因是,从 0 到 k 的数字可以表示的最大数字必须比仅使用数字 k+1 可以表示的最小数字低 1。在二进制中,0111 必须比 1000 小 1。在十进制中,099999 必须比 100000 小 1。
编码为可变基数
后续数字之间的间距恰好为 1 是重要规则。意识到这一点,我们可以用一个可变基数来表示我们的索引序列。每个数字的基数是该数字的不同可能性的数量。对于十进制,每个数字都有 10 种可能性,对于我们的系统,最右边的数字有 1 种可能性,最左边的数字有 n 种可能性。但由于最右边的数字(我们序列中的最后一个数字)始终为 0,因此我们将其省略。这意味着我们剩下以 2 到 n 为基数。一般来说,第 k 个数字的底数为 b[k] = k + 2。数字 k 允许的最大值是 h[k] = b[k] - 1 = k + 1。
我们关于数字权重 w[k] 的规则要求 h[i] * w[i] 的总和,其中 i 从 i = 0 到 i = k,等于 1 * w[k+1]。反复声明,w[k+1] = w[k] + h[k] * w[k] = w[k]*(h[k] + 1)。第一个权重 w[0] 应该始终为 1。从那里开始,我们有以下值:
k h[k] w[k]
0 1 1
1 2 2
2 3 6
3 4 24
... ... ...
n-1 n n!
(一般关系 w[k-1] = k! 很容易通过归纳证明。)
我们通过转换序列得到的数字将是 s[k] * w[k] 的总和,其中 k 从 0 到 n-1。这里 s[k] 是序列的第 k 个(最右边,从 0 开始)元素。例如,以我们的 {1, 2, 0, 1, 0} 为例,最右边的元素如前所述被剥离:{1, 2, 0, 1}。我们的总和是 1 * 1 + 0 * 2 + 2 * 6 + 1 * 24 = 37。
请注意,如果我们为每个索引取最大位置,我们将有 {4, 3, 2, 1, 0},然后转换为 119。由于我们选择了数字编码中的权重,因此我们不会跳过任何数字,所有数字 0 到 119 都有效。其中正好有 120 个,即 n! 对于我们示例中的 n = 5,恰好是不同排列的数量。所以你可以看到我们的编码数字完全指定了所有可能的排列。
从可变基
解码解码类似于转换为二进制或十进制。常见的算法是这样的:
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;
}
对于我们的可变基数:
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
}
这会将我们的 37 正确解码为 {1, 2, 0, 1} (sequence
将{1, 0, 2, 1}
在此代码示例中,但无论如何......只要您正确索引)。我们只需要在右端添加 0(记住最后一个元素的新位置总是只有一种可能性)来恢复我们的原始序列 {1, 2, 0, 1, 0}。
使用索引序列
排列列表 您可以使用以下算法根据特定的索引序列排列列表。不幸的是,这是一个 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;
}
排列的常见表示
通常,您不会像我们所做的那样不直观地表示排列,而只是通过应用排列后每个元素的绝对位置来表示。我们的示例 {1, 2, 0, 1, 0} for abcde
tocaebd
通常由 {1, 3, 0, 4, 2} 表示。从 0 到 4(或通常为 0 到 n-1)的每个索引在此表示中仅出现一次。
以这种形式应用排列很容易:
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];
}
反转它非常相似:
for (int i = 0; i < n; i++)
{
list[i] = permuted[permutation[i]];
}
从我们的表示转换为公共表示
注意,如果我们使用我们的算法来使用索引序列排列一个列表,并将其应用于恒等排列 {0, 1, 2, ..., n-1},我们得到逆排列,以常用形式表示。(在我们的示例中为{2, 0, 4, 1, 3})。
为了得到非倒置的前置换,我们应用我刚刚展示的置换算法:
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];
}
或者您可以直接应用排列,使用逆排列算法:
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]];
}
请注意,所有处理普通形式排列的算法都是 O(n),而以我们的形式应用排列是 O(n²)。如果您需要多次应用排列,请先将其转换为通用表示。