下面是一个允许选择排列大小的解决方案。例如,除了能够生成10个元素的所有排列之外,它还可以在10个元素之间生成对的排列。此外,它还会置换任意对象的列表,而不仅仅是整数。
function nth_permutation($atoms, $index, $size) {
for ($i = 0; $i < $size; $i++) {
$item = $index % count($atoms);
$index = floor($index / count($atoms));
$result[] = $atoms[$item];
array_splice($atoms, $item, 1);
}
return $result;
}
用法示例:
for ($i = 0; $i < 6; $i++) {
print_r(nth_permutation(['A', 'B', 'C'], $i, 2));
}
// => AB, BA, CA, AC, BC, CB
它是如何工作的?
这背后有一个非常有趣的想法。让我们以列表为例。我们可以通过从中绘制元素来构造排列,就像从一副纸牌中绘制元素一样。最初,我们可以绘制四个元素之一。然后是剩下的三个元素中的一个,依此类推,直到最后我们一无所有。A, B, C, D
以下是一个可能的选择序列。从顶部开始,我们走第三条路,然后是第一条路,第二条路,最后是第一条路。这就是我们的排列#13。
想想看,给定这个选择序列,你如何通过算法得到数字13。然后反转你的算法,这就是你如何从整数重建序列。
让我们尝试找到一种通用方案,用于将一系列选择打包成一个没有冗余的整数,然后将其解压缩回去。
一个有趣的方案称为十进制数系统。“27”可以被认为是从10中选择路径#2,然后在10中选择路径#7。
但每个数字只能编码10个备选方案中的选项。具有固定基数的其他系统(如二进制和十六进制)也只能对固定数量的备选项中的选择序列进行编码。我们想要一个具有可变基数的系统,有点像时间单位,“14:05:29”是从24开始的小时14,从60开始的分钟5,从60开始的第二个29。
如果我们采用通用的数字到字符串和字符串到数字的函数,并欺骗它们使用混合基数,该怎么办?它们不是像 parseInt('beef', 16) 和 (48879).toString(16) 那样取单个基数,而是每个数字取一个基数。
function pack(digits, radixes) {
var n = 0;
for (var i = 0; i < digits.length; i++) {
n = n * radixes[i] + digits[i];
}
return n;
}
function unpack(n, radixes) {
var digits = [];
for (var i = radixes.length - 1; i >= 0; i--) {
digits.unshift(n % radixes[i]);
n = Math.floor(n / radixes[i]);
}
return digits;
}
这甚至有效吗?
// Decimal system
pack([4, 2], [10, 10]); // => 42
// Binary system
pack([1, 0, 1, 0, 1, 0], [2, 2, 2, 2, 2, 2]); // => 42
// Factorial system
pack([1, 3, 0, 0, 0], [5, 4, 3, 2, 1]); // => 42
现在倒过来:
unpack(42, [10, 10]); // => [4, 2]
unpack(42, [5, 4, 3, 2, 1]); // => [1, 3, 0, 0, 0]
这真是太美了。现在,让我们将此参数化数字系统应用于排列问题。我们将考虑 的长度 2 排列。它们的总数是多少?让我们看看:首先我们绘制4个项目中的一个,然后绘制其余3个项目中的一个,这就是绘制2个项目的方法。这 12 种方式可以打包成整数 [0..11]。所以,让我们假装我们已经打包了它们,并尝试解压缩:A, B, C, D
4 * 3 = 12
for (var i = 0; i < 12; i++) {
console.log(unpack(i, [4, 3]));
}
// [0, 0], [0, 1], [0, 2],
// [1, 0], [1, 1], [1, 2],
// [2, 0], [2, 1], [2, 2],
// [3, 0], [3, 1], [3, 2]
这些数字表示选择,而不是原始数组中的索引。[0, 0] 并不意味着采取,它意味着从(即A)中获取项目#0,然后从其余列表(即B)中获取项目#0。得到的排列是 。A, A
A, B, C, D
B, C, D
A, B
另一个例子:[3,2]意味着从(即D)中获取项目#3,然后从其余列表(即C)中获取项目#2。得到的排列是 。A, B, C, D
A, B, C
D, C
此映射称为 Lehmer 代码。让我们将所有这些 Lehmer 代码映射到排列:
AB, AC, AD, BA, BC, BD, CA, CB, CD, DA, DB, DC
这正是我们所需要的。但是,如果您查看该函数,您会注意到它从右到左生成数字(以反转 的操作)。从 3 中选择在从 4 中选择之前被解压缩。这很不幸,因为我们希望先从 4 个元素中进行选择,然后再从 3 个元素中进行选择。如果不能这样做,我们必须首先计算Lehmer代码,将其累积到一个临时数组中,然后将其应用于项目数组以计算实际的排列。unpack
pack
但是,如果我们不关心词典顺序,我们可以假装我们想要从3个元素中进行选择,然后再从4个元素中进行选择。然后,从4中选择将从第一个出来。换句话说,我们将使用 代替 .这个技巧允许计算Lehmer代码的下一个数字,并立即将其应用于列表。这正是工作原理。unpack
unpack(n, [3, 4])
unpack(n, [4, 3])
nth_permutation()
我想提到的最后一件事是,这与阶乘数系统密切相关。再看第一棵树,如果我们想要长度为2的排列而没有重复项,我们可以跳过每秒一次的排列索引。这将为我们提供长度为 4 的 12 个排列,可以将其修剪为长度 2。unpack(i, [4, 3])
for (var i = 0; i < 12; i++) {
var lehmer = unpack(i * 2, [4, 3, 2, 1]); // Factorial number system
console.log(lehmer.slice(0, 2));
}