找到第n个排列而不计算其他排列

时间:2011-10-27 16:00:23

标签: php algorithm math permutation

给定表示置换原子的N个元素的数组,是否有类似的算法:

function getNthPermutation( $atoms, $permutation_index, $size )

其中$atoms是元素数组,$permutation_index是排列的索引,$size是排列的大小。

例如:

$atoms = array( 'A', 'B', 'C' );
// getting third permutation of 2 elements
$perm = getNthPermutation( $atoms, 3, 2 );

echo implode( ', ', $perm )."\n";

会打印:

B, A

直到$ permutation_index才计算每个排列?

我听到了关于事实排列的一些事情,但我发现的每一个实现都会给出一个具有相同V大小的排列,这不是我的情况。

感谢。

8 个答案:

答案 0 :(得分:43)

正如RickyBobby所说,在考虑排列的词典顺序时,你应该利用因子分解。

从实际的角度来看,这就是我的看法:

  • 执行某种欧几里德分割,除了使用因子数字,从(n-1)!(n-2)!开始,等等。
  • 将商保留在数组中。 i - 商应该是0n-i-1之间的数字,其中i0n-1
  • 此数组您的排列。问题是每个商不关心以前的值,所以你需要调整它们。更明确地说,您需要将每个值递增多次,因为之前的值较低或相等。

以下C代码应该让您了解其工作原理(n是条目数,i是排列的索引):

/**
 * @param n The number of entries
 * @param i The index of the permutation
 */
void ithPermutation(const int n, int i)
{
   int j, k = 0;
   int *fact = (int *)calloc(n, sizeof(int));
   int *perm = (int *)calloc(n, sizeof(int));

   // compute factorial numbers
   fact[k] = 1;
   while (++k < n)
      fact[k] = fact[k - 1] * k;

   // compute factorial code
   for (k = 0; k < n; ++k)
   {
      perm[k] = i / fact[n - 1 - k];
      i = i % fact[n - 1 - k];
   }

   // readjust values to obtain the permutation
   // start from the end and check if preceding values are lower
   for (k = n - 1; k > 0; --k)
      for (j = k - 1; j >= 0; --j)
         if (perm[j] <= perm[k])
            perm[k]++;

   // print permutation
   for (k = 0; k < n; ++k)
      printf("%d ", perm[k]);
   printf("\n");

   free(fact);
   free(perm);
}

例如,ithPermutation(10, 3628799)按预期打印十个元素的最后一个排列:

9 8 7 6 5 4 3 2 1 0

答案 1 :(得分:28)

这是一个允许选择排列大小的解决方案。例如,除了能够生成10个元素的所有排列之外,它还可以生成10个元素中的对的排列。它还会置换任意对象的列表,而不仅仅是整数。

这是PHP,但也有JavaScriptHaskell强制执行。

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。我们可以通过从卡片中抽取元素来构造排列。最初我们可以绘制四个元素中的一个。然后是其余三个元素中的一个,依此类推,直到最后我们什么都没有留下。

Decision tree for permutations of 4 elements

这是一种可能的选择顺序。从顶部开始,我们采取第三条路径,然后是第一条路径,第二条路径,最后是第一条路径。这就是我们的排列#13。

考虑一下如果选择这个序列,你会在算法上得到十三个数字。然后反转你的算法,这就是你如何从整数重构序列。

让我们试着找到一个将选择序列打包成没有冗余的整数的一般方案,然后将其解包。

一个有趣的方案叫做十进制数系统。 “27”可以被认为是从10中选择路径#2,然后从10中选择路径#7。

Decision three for number 27 in decimal

但每个数字只能编码来自10个替代品的选项。具有固定基数的其他系统,如二进制和十六进制,也只能编码来自固定数量的备选方案的选择序列。我们想要一个具有可变基数的系统,类似于时间单位,“14:05:29”是小时14从24,分钟5从60,第二个29从60。

如果我们采用通用的数字到字符串和字符串到数字的功能,并欺骗它们使用混合基数怎么办?它们不是采用单个基数,如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]

这太美了。现在让我们将这个参数数字系统应用于排列问题。我们将考虑A, B, C, D的长度为2的排列。它们的总数是多少?让我们看看:首先我们绘制4个项目中的一个,然后是其余3个中的一个,即4 * 3 = 12方式绘制2个项目。这12种方式可以打包成整数[0..11]。所以,让我们假装我们已经打包它们,并尝试解压缩:

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, A,这意味着从A, B, C, D(即A)中获取项目#0,然后从剩余列表B, C, D中获取项目#0(即B) 。结果排列为A, B

另一个例子:[3,2]意味着从A, B, C, D(即D)中获取项目#3,然后从剩余列表A, B, C中获取项目#2(即C)。结果排列为D, C

此映射称为Lehmer code。让我们将所有这些Lehmer代码映射到排列:

AB, AC, AD, BA, BC, BD, CA, CB, CD, DA, DB, DC

这正是我们所需要的。但是如果你看一下unpack函数,你会注意到它从右到左产生数字(以反转pack的动作)。 3中的选择在从4中选择之前被解压缩。这是不幸的,因为我们想在从3中选择之前从4个元素中进行选择。如果不能这样做,我们必须首先计算Lehmer代码,将其累积到临时数组中,然后将其应用于项目数组以计算实际排列。

但是如果我们不关心字典顺序,我们可以假装我们要在选择4之前从3个元素中进行选择。然后从unpack首先选择4。换句话说,我们将使用unpack(n, [3, 4])代替unpack(n, [4, 3])。这个技巧允许计算Lehmer代码的下一个数字并立即将其应用于列表。这就是nth_permutation()的工作方式。

我要提到的最后一件事是unpack(i, [4, 3])与阶乘数系统密切相关。再看一下第一棵树,如果我们想要长度为2而没有重复的排列,我们可以跳过每一秒的排列索引。这将给我们12个长度为4的排列,可以修剪为长度为2。

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));
}

答案 2 :(得分:15)

这取决于你的方式&#34;排序&#34;你的排列(例如词典顺序)。

一种方法是factorial number system,它会在[0,n!]和所有排列之间给你一个双射。

然后对于[0,n!]中的任何数字,你可以计算第i个排列而不计算其他数字。

这种因子写作基于以下事实:[0和n!]之间的任何数字都可写为:

SUM( ai.(i!) for i in range [0,n-1]) where ai <i 

(它与基本分解非常相似)

有关此分解的更多信息,请查看此主题:https://math.stackexchange.com/questions/53262/factorial-decomposition-of-integers

希望有所帮助


正如此wikipedia article所述,此方法相当于计算lehmer code

  

生成n的排列的一种显而易见的方法是生成值   Lehmer代码(可能使用阶乘数系统)   表示整数到n!),并将它们转换为   相应的排列。然而,后一步,而   直截了当,难以有效实施,因为它需要   n从序列中选择每个选项并从中删除,   处于任意位置;的明显表示   序列作为数组或链表,都需要(针对不同的   关于执行转换的n2 / 4操作的原因)。随着n   可能相当小(特别是如果所有的一代   需要排列)这不是一个问题,而是它   事实证明,无论是随机还是系统生成都有   简单的替代品,做得更好。出于这个原因   虽然肯定有可能采用一种特殊的方法,但似乎并没有用   允许从Lehmer执行转换的数据结构   代码在O(n log n)时间内排列。

因此,对于一组n元素,你可以做的最好的是O(n ln(n)),它具有适应的数据结构。

答案 3 :(得分:7)

这是一种在线性时间内在排列和排名之间进行转换的算法。但是,它使用的排名不是词典。这很奇怪,但一致。我将给出两个函数,一个从一个等级转换为一个置换,另一个用于反转。

首先,取消(从排名到排列)

Initialize:
n = length(permutation)
r = desired rank
p = identity permutation of n elements [0, 1, ..., n]

unrank(n, r, p)
  if n > 0 then
    swap(p[n-1], p[r mod n])
    unrank(n-1, floor(r/n), p)
  fi
end

接下来,排名:

Initialize:
p = input permutation
q = inverse input permutation (in linear time, q[p[i]] = i for 0 <= i < n)
n = length(p)

rank(n, p, q)
  if n=1 then return 0 fi
  s = p[n-1]
  swap(p[n-1], p[q[n-1]])
  swap(q[s], q[n-1])
  return s + n * rank(n-1, p, q)
end

这两者的运行时间是O(n)。

有一篇很好的,可读的论文解释了为什么这样有效:排名&amp;通过Myrvold&amp; amp;和线性时间中的排名排列。 Ruskey,信息处理快报,第79卷,第6期,2001年9月30日,第281-284页。

http://webhome.cs.uvic.ca/~ruskey/Publications/RankPerm/MyrvoldRuskey.pdf

答案 4 :(得分:5)

这是python中的一个简短且非常快(元素数量为线性)的解决方案,适用于任何元素列表(下例中的13个首字母):

from math import factorial

def nthPerm(n,elems):#with n from 0
    if(len(elems) == 1):
        return elems[0]
    sizeGroup = factorial(len(elems)-1)
    q,r = divmod(n,sizeGroup)
    v = elems[q]
    elems.remove(v)
    return v + ", " + ithPerm(r,elems)

示例:

letters = ['a','b','c','d','e','f','g','h','i','j','k','l','m']

ithPerm(0,letters[:])          #--> a, b, c, d, e, f, g, h, i, j, k, l, m
ithPerm(4,letters[:])          #--> a, b, c, d, e, f, g, h, i, j, m, k, l
ithPerm(3587542868,letters[:]) #--> h, f, l, i, c, k, a, e, g, m, d, b, j

注意:我提供letters[:]letters的副本)而不是字母,因为该函数会修改其参数elems(删除所选元素)

答案 5 :(得分:2)

以下代码计算给定n的第k个排列。

即n = 3。 各种排列是     123     132     213     231     312     321

如果k = 5,则返回312。 换句话说,它给出了第k个字典编排。

#define OPT_STRING_BASE "haspvb"

#ifdef HAVE_WIFI
#define OPT_STRING_WIFI "mw"
#else
#define OPT_STRING_WIFI
#endif // HAVE_WIFI

#ifdef HAVE_IMEI
#define OPT_STRING_IMEI "i"
#else
#define OPT_STRING_IMEI
#endif // HAVE_IMEI

#define OPT_STRING (OPT_STRING_BASE OPT_STRING_WIFI OPT_STRING_IMEI)

答案 6 :(得分:0)

可以计算。这是一个为您完成的C#代码。

#If

答案 7 :(得分:-1)

如果将所有排列存储在内存中,例如存储在数组中,您应该能够在O(1)时间内将它们一次退出。

这意味着您必须存储所有排列,因此如果计算所有排列需要花费相当长的时间,或者存储它们需要相当大的空间,那么这可能不是解决方案。

我的建议是尝试它,如果它太大/太慢,那就回来吧 - 如果一个天真的人能够完成这项工作,那就没有必要寻找一个“聪明”的解决方案。