如何计算线性时间内的排列,扭曲

时间:2009-01-27 15:38:35

标签: algorithm permutation

我在Java中有一个资源调度问题需要对事物进行排序,但是对于彼此相邻的资源存在限制。一个很好的类比是一串“数字”,其中只有某些数字可以彼此相邻。我的解决方案是递归的,适用于小字符串,但运行时间是O(X ^ N),其中X是可能的数字(基数),N是字符串的长度。它很快变得无法管理。

使用下面的兼容性矩阵,以下是允许字符串的几个示例
长度为1:0,1,2,3,4 长度为2:02,03,14,20,30,41
长度为3:020,030,141,202,203,302,303,414

     0  1  2  3  4
   ---------------------
0|  0  0  1  1  0
1|  0  0  0  0  1
2|  1  0  0  0  0
3|  1  0  0  0  0
4|  0  1  0  0  0

我计算所有长度为N的字符串的解决方案是以空字符串开头,置换第一个数字,并对所有长度为N-1的字符串进行递归调用。递归调用检查添加的最后一个数字,并尝试可以在该数字旁边的所有排列。有一些优化,所以我不会每次尝试和替换00,01,04,例如 - 仅02,03,但性能仍然很差,因为它从基数5(示例)扩展到基数4000. / p>

除了试图列举所有排列之外,还有什么想法可以更好地计算排列?

5 个答案:

答案 0 :(得分:19)

如果您只想要一定长度的字符串数,您可以将兼容性矩阵与其自身相乘几次,然后将其值相加。

  

n = 字符串的长度
   A = 兼容性矩阵
  可能的字符串数量 = A n -1

的总和

一些例子:

n = 1
| 1 0 0 0 0 |
| 0 1 0 0 0 |
| 0 0 1 0 0 |
| 0 0 0 1 0 |
| 0 0 0 0 1 |
sum: 5

n = 3
| 2 0 0 0 0 |
| 0 1 0 0 0 |
| 0 0 1 1 0 |
| 0 0 1 1 0 |
| 0 0 0 0 1 |
sum: 8

n = 8
| 0 0 8 8 0 |
| 0 0 0 0 1 |
| 8 0 0 0 0 |
| 8 0 0 0 0 |
| 0 1 0 0 0 |
sum: 34

原始矩阵(行 i ,列 j )可以被认为是以符号 i 开头的字符串数量,以及其下一个符号是符号 j 。或者,您可以将其视为长度 2 的字符串数,以符号 i 开头,以符号 j 结束。

矩阵乘法保留此不变量,因此在取幂后, A n -1 将包含以符号 i ,长度 n ,以符号 j 结尾。

有关更快计算矩阵幂的算法,请参阅Wikipedia: Exponentiation by squaring

(感谢stefan.ciobaca)

这个具体案例简化为公式:

  

可能的字符串数量 = f n )= 4 +Σ k = 1 .. n 2 k -1 / 2 = f n -1)+ 2 n -1 / 2 < /子>⌋

n       f(n)
----    ----
   1       5
   2       6
   3       8
   4      10
   5      14
   6      18
   7      26
   8      34
   9      50
  10      66

答案 1 :(得分:3)

您是否只想知道您可以使用给定矩阵中的规则构建给定长度的字符串数?如果是这样,这样的方法应该有效:

n = 5
maxlen = 100

combine = [
      [0, 0, 1, 1, 0],
      [0, 0, 0, 0, 1],
      [1, 0, 0, 0, 0],
      [1, 0, 0, 0, 0],
      [0, 1, 0, 0, 0]
   ]

# counts of strings starting with 0,1,...,4, initially for strings of length one:
counts = [1, 1, 1, 1, 1]

for size in range(2, maxlen+1):
   # calculate counts for size from count for (size-1)
   newcount = []
   for next in range(n):
      total = 0
      for head in range(n):
         if combine[next][head]:
            # |next| can be before |head|, so add the counts for |head|
            total += counts[head]
      # append, so that newcount[next] == total
      newcount.append(total)
   counts = newcount
   print "length %i: %i items" % (size, sum(counts))

答案 2 :(得分:2)

您的算法似乎是最佳的。

你是如何使用这些排列的?您是将它们累积在一个列表中,还是逐个使用它?由于存在大量这样的排列,所以性能不佳可能是由于大量内存使用(如果你正在收集所有这些)或者它只需要花费很多时间。你无法在琐碎的时间内完成数十亿次循环。

回复评论:

如果您只想计算它们,那么您可以使用动态编程:

设count [n] [m]为一个数组,其中count [l] [j]是长度为l且以j结尾的这种排列的数量,

然后count [l] [i] = count [l-1] [i1] + count [l-1] [i2] + ...,其中i1,i2,...是可以先于的数字i(可以保存在预先计算的数组中)。

每个计数单元可以通过求和K个数来填充(K取决于兼容矩阵),因此复杂度为O(KMN),M是置换的长度,N是总位数。 / p>

答案 3 :(得分:1)

也许我不理解这一点,但是如果有一个列表,每个数字都有一个可以跟随它的有效数字列表,那么这不会有用。

然后生成的例程将获取累计结果,数字编号和当前数字。类似的东西:

// not really Java - and you probably don't want chars, but you'll fix it
void GenerateDigits(char[] result, int currIndex, char currDigit)
{
    if (currIndex == kMaxIndex) {
        NotifyComplete(result);
        return;
    }
    char[] validFollows = GetValidFollows(currDigit); // table lookup
    foreach (char c in validFollows) {
        result[currIndex] = c;
        GenerateDigits(result, currIndex+1, c);
    }
}

复杂性随着要生成的位数的增加而增加,但该函数取决于任何一位数的有效跟随的总数。如果每个数字的跟随总数相同,比如k,那么生成所有可能的排列的时间将是O(k ^ n),其中n是数字的位数。对不起,我无法改变数学。在基数10中生成n个数字的时间是10 ^ n。

答案 4 :(得分:1)

我不确定你在问什么,但因为有可能是n!一串n个数字的排列,你将无法比n!更快地列出它们。我不确定你认为你有一个O(n ^ 2)的运行时间。