具有恰好k个反转的n元素排列的数量

时间:2013-10-15 03:49:11

标签: algorithm permutation dynamic-programming combinatorics discrete-mathematics

我正在努力有效地解决SPOJ Problem 64: Permutations

  

设A = [a1,a2,...,an]是整数1,2,...,n的排列。一双   索引(i,j),1< = i< = j< = n,是置换A的倒置   AI> AJ。我们给出整数n> 0且k> = 0。数量是多少   n元素排列恰好包含k个反转?

     

例如,正好为1的4元素排列的数量   反转等于3。

为了使给定的例子更容易看到,这里有三个4元素排列,正好有1个反转:

(1, 2, 4, 3)
(1, 3, 2, 4)
(2, 1, 3, 4)

在第一个排列中,4> 3和4的指数小于3的指数。这是单个反转。由于排列只有一次反转,这是我们试图计算的排列之一。

对于任何给定的n个元素序列,排列的数量是阶乘(n)。因此,如果我使用强力n 2 计算每个排列的反转次数,然后检查它们是否等于k,那么这个问题的解决方案将具有时间复杂度O( n!* n 2 )。

<小时/>

以前的研究

先前在StackOverflow上询问了此问题的子问题here。给出了使用合并排序的O(n log n)解决方案,该解决方案计算排列中的反转次数。但是,如果我使用该解决方案来计算每个排列的反转次数,我仍然会得到O(n!* n log n)的时间复杂度,在我看来仍然非常高。

exact question was also asked previously on Stack Overflow but it received no answers.

<小时/> 我的目标是避免迭代所有排列所带来的因子复杂性。理想情况下,我想要一个数学公式,为任何n和k得出这个答案,但我不确定是否存在。

如果没有解决这个问题的数学公式(我有点怀疑),那么我也看到人们提示有效的动态编程解决方案是可能的。使用DP或其他方法,我真的想制定一个比O(n!* n log n)更有效的解决方案,但我不确定从哪里开始。

欢迎任何提示,评论或建议。

编辑:我已经用DP方法计算Mahonian numbers来回答下面的问题。

5 个答案:

答案 0 :(得分:11)

解决方案需要一些解释。  让我们用n个项具有恰好k个反转的排列数表示  由我(n,k)

现在我(n,0)总是1.对于任何n,存在一个且只有一个具有0的置换  反转,即当序列越来越分类时

现在我(0,k)总是0,因为我们没有序列本身

现在找到I(n,k)让我们举一个包含4个元素的序列的例子  {1,2,3,4}

下面n = 4的

是枚举的排列,并按反演次数分组

|___k=0___|___k=1___|___k=2___|___k=3___|___k=4___|___k=5___|___k=6___|
| 1234    | 1243    | 1342    | 1432    | 2431    | 3421    | 4321    |
|         | 1324    | 1423    | 2341    | 3241    | 4231    |         |
|         | 2134    | 2143    | 2413    | 3412    | 4312    |         |
|         |         | 2314    | 3142    | 4132    |         |         |
|         |         | 3124    | 3214    | 4213    |         |         |
|         |         |         | 4123    |         |         |         |
|         |         |         |         |         |         |         |
|I(4,0)=1 |I(4,1)=3 |I(4,2)=5 |I(4,3)=6 |I(4,4)=5 |I(4,5)=3 |I(4,6)=1 |
|         |         |         |         |         |         |         |

现在找到n = 5的排列数和每个可能的k 我们可以通过插入第n个(最大)从I(4,k)导出递归I(5,k) 元素(5)在先前排列的每个排列中的某处, 因此得到的反转次数为k

例如,I(5,4)只不过序列的排列数{1,2,3,4,5} 每个正好有4个倒置。 让我们观察上面的I(4,k),直到列k = 4,反转次数<= 4 现在让我们放置元素5,如下所示

|___k=0___|___k=1___|___k=2___|___k=3___|___k=4___|___k=5___|___k=6___|
| |5|1234 | 1|5|243 | 13|5|42 | 143|5|2 | 2431|5| | 3421    | 4321    |
|         | 1|5|324 | 14|5|23 | 234|5|1 | 3241|5| | 4231    |         |
|         | 2|5|134 | 21|5|43 | 241|5|3 | 3412|5| | 4312    |         |
|         |         | 23|5|14 | 314|5|4 | 4132|5| |         |         |
|         |         | 31|5|24 | 321|5|4 | 4213|5| |         |         |
|         |         |         | 412|5|3 |         |         |         |
|         |         |         |         |         |         |         |
|    1    |    3    |    5    |    6    |    5    |         |         |
|         |         |         |         |         |         |         |

包含5的每个上述排列恰好有4个反转。 所以4个反演的总置换I(5,4)= I(4,4)+ I(4,3)+ I(4,2)+ I(4,1)+ I(4,0)                                                   = 1 + 3 + 5 + 6 + 5 = 20

同样来自I(4,k)的I(5,5)

|___k=0___|___k=1___|___k=2___|___k=3___|___k=4___|___k=5___|___k=6___|
|   1234  | |5|1243 | 1|5|342 | 14|5|32 | 243|5|1 | 3421|5| | 4321    |
|         | |5|1324 | 1|5|423 | 23|5|41 | 324|5|1 | 4231|5| |         |
|         | |5|2134 | 2|5|143 | 24|5|13 | 341|5|2 | 4312|5| |         |
|         |         | 2|5|314 | 31|5|44 | 413|5|2 |         |         |
|         |         | 3|5|124 | 32|5|14 | 421|5|3 |         |         |
|         |         |         | 41|5|23 |         |         |         |
|         |         |         |         |         |         |         |
|         |    3    |    5    |    6    |    5    |    3    |         |
|         |         |         |         |         |         |         |

所以5次反转的总置换I(5,5)= I(4,5)+ I(4,4)+ I(4,3)+ I(4,2)+ I(4,1) )                                                   = 3 + 5 + 6 + 5 + 3 = 22

所以I(n, k) = sum of I(n-1, k-i) such that i < n && k-i >= 0

此外,当序列按降序排序时,k可以达到n *(n-1)/ 2 https://secweb.cs.odu.edu/~zeil/cs361/web/website/Lectures/insertion/pages/ar01s04s01.html http://www.algorithmist.com/index.php/SPOJ_PERMUT1

#include <stdio.h>

int dp[100][100];

int inversions(int n, int k)
{
    if (dp[n][k] != -1) return dp[n][k];
    if (k == 0) return dp[n][k] = 1;
    if (n == 0) return dp[n][k] = 0;
    int j = 0, val = 0;
    for (j = 0; j < n && k-j >= 0; j++)
        val += inversions(n-1, k-j);
    return dp[n][k] = val;
}

int main()
{
    int t;
    scanf("%d", &t);
    while (t--) {
        int n, k, i, j;
        scanf("%d%d", &n, &k);
        for (i = 1; i <= n; i++)
            for (j = 0; j <= k; j++)
                dp[i][j] = -1;
        printf("%d\n", inversions(n, k));
    }
    return 0;
}

答案 1 :(得分:7)

一天后,我设法使用动态编程来解决问题。我提交了它,我的代码被SPOJ接受了,所以我想我会在这里与任何对未来感兴趣的人分享我的知识。

在查看Wikipedia page which discusses inversion in discrete mathematics后,我在页面底部找到了一个有趣的推荐。

  

具有k个反转的n个元素的排列数; Mahonian   数字:A008302

我点击了the link to OEIS,它向我展示了一个无限的整数序列,称为Mahonian数的三角形。

  

1,1,1,1,2,2,1,1,3,5,6,5,3,1,1,4,9,15,20,22,20,15,   9,4,1,1,5,14,29,49,71,90,101,101,90,71,49,29,14,5,1,   1,6,20,29,98,169,259,359,455,531,573,573,513,455,359,   259,169,98,49,20,6,1。 。

我很好奇这些数字是什么,因为他们似乎对我很熟悉。然后我意识到我之前已经看到过后续的1,3,5,6,5,3,1。事实上,这是几对(n,k)的问题的答案,即(4,0),(4,1),(4,2),(4,3),(4,4) ,(4,5),(4,6)。我查看了这个子序列两边的内容,并惊讶地发现,对于n&lt;所有这些都是有效的(即大于0的排列)答案。 4和n> 4。

序列的公式如下:

  

Product_ {i = 0..n-1}(1 + x + ... + x ^ i)扩展中的系数

这对我来说很容易理解和验证。我基本上可以接受任何n并插入公式。那么x k 项的系数将是(n,k)的答案。

我将展示n = 3的示例。

(x0)(x0 + 1)(x0 + x1 + x2) = (1)(1 + x)(1 + x + x2) = (1 + x)(1 + x + x2) = 1 + x + x + x2 + x2 + x3 = 1 + 2x + 2x2 + x3

最终扩展为1 + 2x + 2x2 + x3,并且对于k = 0,1,2,3,x k 项的系数分别为1,2,2和1。这恰好是3元素排列的所有有效反转数。

1,2,2,1是Mahonian数字的第3行,如下表所示:

1
1 1
1 2 2 1
1 3 5 6 5 3 1
etc.

所以基本上计算我的答案归结为简单地计算第n个Mahonian行并将第k个元素从k开始于0并且如果索引超出范围则打印0。这是一个自下而上的动态编程的简单情况,因为每个第i行可用于轻松计算第i + 1行。

以下是我使用的Python解决方案,仅在0.02秒内运行。对于给定的测试用例,此问题的最长时间限制为3秒,之前我收到超时错误,因此我认为此优化相当不错。

def mahonian_row(n):
    '''Generates coefficients in expansion of 
    Product_{i=0..n-1} (1+x+...+x^i)
    **Requires that n is a positive integer'''
    # Allocate space for resulting list of coefficients?
    # Initialize them all to zero?
    #max_zero_holder = [0] * int(1 + (n * 0.5) * (n - 1))

    # Current max power of x i.e. x^0, x^0 + x^1, x^0 + x^1 + x^2, etc.
    # i + 1 is current row number we are computing
    i = 1
    # Preallocate result
    # Initialize to answer for n = 1
    result = [1]
    while i < n:
        # Copy previous row of n into prev
        prev = result[:]
        # Get space to hold (i+1)st row
        result = [0] * int(1 + ((i + 1) * 0.5) * (i))
        # Initialize multiplier for this row
        m = [1] * (i + 1)
        # Multiply
        for j in range(len(m)):
            for k in range(len(prev)):
                result[k+j] += m[j] * prev[k]
        # Result now equals mahonian_row(i+1)
        # Possibly should be memoized?
        i = i + 1
    return result


def main():
    t = int(raw_input())
    for _ in xrange(t):
        n, k = (int(s) for s in raw_input().split())
        row = mahonian_row(n)
        if k < 0 or k > len(row) - 1:
            print 0
        else:
            print row[k]


if __name__ == '__main__':
    main()

我不知道时间复杂度,但我绝对肯定这段代码可以通过memoization进行改进,因为有10个给定的测试用例,以前的测试用例的计算可以用来“欺骗”未来测试用例。我将在未来进行优化,但希望今后这个问题的答案将有助于任何人在未来尝试这个问题,因为它避免了生成和迭代所有排列的天真的因子复杂性方法。

答案 2 :(得分:6)

如果存在动态编程解决方案,可能有一种方法可以逐步完成,使用长度为n的排列结果来帮助获得长度为n + 1的排列结果。

给定长度n - 值1-n的排列,通过在n + 1个可能位置加上值(n + 1),可以得到长度为n + 1的排列。 (n + 1)大于1-n中的任何一个,所以当你这样做时你创建的反转次数取决于你添加它的位置 - 在最后位置添加它并且你不创建反转,在最后添加它但是一个位置,你创建一个反转,依此类推 - 回顾n = 4个案例,有一个反转来检查这个。

因此,如果您考虑n + 1个地方中的一个,您可以添加(n + 1),如果您将其添加到地方j从右边开始计数,那么最后一个位置为位置0,其中创建的K反转的排列数是在n个地方用Kj反演的排列数。

因此,如果在每个步骤中计算所有可能K的K反转的排列数,则可以使用长度为n的K反转的排列数来更新长度为n + 1的K反转的排列数。

答案 3 :(得分:0)

计算这些系数的主要问题是所得产品的顺序大小。多项式乘积i = 1,2,..,n {(1 + x)。(1 + x + x ^ 2)....(1 + x + x ^ 2 + .. + x ^ i)+ ......(1 + x + x ^ 2 + ... + x ^ n)将具有等于n *(n + 1)的顺序。因此,这对该过程施加了限制性计算限制。如果我们使用一个过程,其中产品的n-1的先前结果用于计算n的产品的过程中,我们正在查看(n-1)* n个整数的存储。可以使用递归过程,该过程将慢得多,并且再次限制为小于整数的公共大小的平方根的整数。以下是针对此问题的一些粗略且准备好的递归代码。函数mahonian(r,c)返回第r个乘积的第c个系数。但同样,对于大于100的大型产品来说,它的速度非常慢。运行它可以看出递归显然不是答案。

unsigned int numbertheory::mahonian(unsigned int r, unsigned int c)
  {
      unsigned int result=0;
      unsigned int k;

     if(r==0 && c==0)
       return 1;
     if( r==0 && c!=0)
      return 0;

   for(k=0; k <= r; k++)
       if(r > 0 && c >=k)
           result = result + mahonian(r-1,c-k);

   return result;

}

作为一个感兴趣的问题,我已经包含以下这是Sashank的c ++版本,它比我的递归示例快得多。注意我使用armadillo库。

uvec numbertheory::mahonian_row(uword n){
 uword i = 2;
 uvec current;
 current.ones(i);
 uword current_size;
 uvec prev;
 uword prev_size;

 if(n==0){
   current.ones(1);
   return current;
 }

 while (i <= n){                  // increment through the rows
   prev_size=current.size();     // reset prev size to current size
   prev.set_size(prev_size);     // set size of prev vector
   prev= current;                //copy contents of current to prev vector
   current_size =1+ (i*(i+1)/2);      // reset current_size
   current.zeros(current_size);    // reset current vector with zeros

   for(uword j=0;j<i+1; j++)       //increment through current vector
      for(uword k=0; k < prev_size;k++)
         current(k+j) += prev(k);
   i++;                                        //increment to next row
}
return current;                                //return current vector
 }

 uword numbertheory::mahonian_fast(uword n, uword c) {
**This function returns the coefficient of c order of row n of
**the Mahonian numbers
    // check for input errors
    if(c >= 1+ (n*(n+1)/2)) {
        cout << "Error. Invalid input parameters" << endl;
   }
   uvec mahonian;
   mahonian.zeros(1+ (n*(n+1)/2));
   mahonian = mahonian_row(n);
   return mahonian(c);
 }

答案 4 :(得分:0)

我们可以利用动态编程来解决此问题。我们有n个地方要填充从1到n的数字,_ _ _ _ _ _ _取n = 7,那么在第一个地方我们最多可以实现n-1个反转,并且至少可以达到0,类似地,第二个地方可以一般而言,无论我们之前选择的数字是多少,我们都能在第ith个索引处实现最多n-2个求反和至少0个。 我们的递归公式将如下所示:

f(n,k)= f(n-1,k)+ f(n-1,k-1)+ f(n-1,k-2).......... ... f(n-1,max(0,k-(n-1))          无反转一反转二反转n-1反转 我们可以通过将集合(1,n)中最小的剩余数放置为最小来实现0反转 通过放置第二倒数来进行1次反演,等等,

我们的递归公式的基本条件将是。

if(i == 0 && k == 0)返回1(有效排列)

if(i == 0 && k!= 0)返回0(无效排列)。

如果我们绘制递归树,我们将看到子问题重复多次,因此使用记忆化将复杂度降低到O(n * k)。