【问题标题】:Number of n-element permutations with exactly k inversions恰好有 k 个反转的 n 元素排列的数量
【发布时间】:2013-10-22 18:31:02
【问题描述】:

我正在尝试有效地解决SPOJ Problem 64: Permutations

令 A = [a1,a2,...,an] 是整数 1,2,...,n 的排列。一双 指数 (i,j) 的 1aj。我们得到整数 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)。因此,如果我使用蛮力 n2 方法计算每个排列的反转次数,然后检查它们是否等于 k,则该问题的解决方案将具有时间复杂度 O( n! * n2)。


以前的研究

之前在 StackOverflow 上询问过 here 这个问题的一个子问题。给出了一个使用归并排序的 O(n log n) 解决方案,它计算 single 排列中的反转次数。但是,如果我使用该解决方案来计算每个排列的反转次数,我仍然会得到 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) 更有效的解决方案,但我不确定从哪里开始。

欢迎任何提示、cmets 或建议。

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

【问题讨论】:

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


    【解决方案1】:

    如果有动态规划解决方案,可能有一种方法可以逐步完成,使用长度为 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 反转的排列数。

    【讨论】:

    • 我认为您的答案相当于使用动态规划计算 Product_{i=0..n-1} (1+x+...+x^i) 展开的系数,以便第 n 行的结果可用于计算第 n+1 行的结果。如果您有兴趣,请随时check out my answer。我给了你+1。 :)
    • 我没有检查你的答案,但我一点也不感到惊讶。 algo.inria.fr/flajolet/Publications/book.pdf 有一本可下载的书,它声称可以教你如何通过生成函数解决大量此类问题,但我还没有找到时间或动力去解决它。
    【解决方案2】:

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

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

    n个元素的k个反转的排列数;马霍尼安 号码:A008302

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

    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、49、98、169、259、359、455、531、573、573、531、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 4,它们都是有效的(即大于 0 个排列)答案。

    序列的公式为:

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

    这对我来说很容易理解和验证。我基本上可以取任何 n 并插入公式。那么 xk 项的系数将是 (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 + 2x<sup>2</sup> + x<sup>3</sup>,xk 项的系数分别为 k = 0、1、2、3 时的 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 从 0 开始获取第 k 个元素,如果索引超出范围则打印 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 个给定的测试用例,并且以前测试用例的计算可以用来“欺骗”未来的测试用例.我将在未来进行优化,但希望当前状态下的这个答案能帮助任何人在未来尝试这个问题,因为它避免了生成和迭代所有排列的天真的阶乘复杂性方法。

    【讨论】:

      【解决方案3】:

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

      现在 I(n, 0) 总是 1。对于任何 n 存在一个且只有一个具有 0 的排列 倒置,即当序列越来越排序时

      现在 I(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) element(5) 在先前排列中的每个排列中的某处, 所以得到的反转次数是k

      例如,I(5,4) 只不过是序列 {1,2,3,4,5} 的排列数 每个恰好有 4 个反转。 让我们观察上面的 I(4, k) 直到列 k = 4,反转的数量是

      |___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 &lt; n &amp;&amp; k-i &gt;= 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;
      }
      

      【讨论】:

      • 很好的解释:)
      • 是的,但是有什么办法可以在 n log n 中做到这一点
      【解决方案4】:

      计算这些系数的一个主要问题是结果乘积的阶数。多项式积 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 的 Product 的先前结果用于计算 n 的 Product 的过程,我们正在查看 (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++ 版本,它比我的递归示例快得多。注意我使用的是犰狳库。

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

      【讨论】:

        【解决方案5】:

        我们可以利用动态规划来解决这个问题。我们有 n 个地方可以填充从 1 到 n 的数字,_ _ _ _ _ _ 取 n=7,那么在第一个地方我们可以实现最多 n-1 反转和至少 0 ,同样对于第二个地方我们可以实现最多 n-2 次反转且至少为 0,一般而言,我们可以在第 i 个索引处实现最多 ni 次反转,而与我们之前放置的数字的选择无关。 我们的递归公式看起来像:

        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)) 无反演 1 反演 2 反演 n-1 反演 我们可以通过从集合 (1,n) 中放置最小的剩余数字来实现 0 反转 通过放置第二小等来进行 1 次反转,

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

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

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

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-11-15
          • 1970-01-01
          • 2021-10-02
          • 1970-01-01
          • 1970-01-01
          • 2020-10-27
          相关资源
          最近更新 更多