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

Sha*_*ank 16 algorithm permutation dynamic-programming combinatorics discrete-mathematics

我正在努力有效地解决SPOJ问题64:排列.

设A = [a1,a2,...,an]是整数1,2,...,n的排列.如果ai> aj,则一对索引(i,j),1 <= i <= j <= n,是置换A的反转.我们给出整数n> 0和k> = 0.包含正好k次反转的n元素排列的数量是多少?

例如,恰好1次反转的4元素排列的数量等于3.

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

(1, 2, 4, 3)
(1, 3, 2, 4)
(2, 1, 3, 4)
Run Code Online (Sandbox Code Playgroud)

在第一个排列中,4> 3且指数4小于3的指数.这是单个反转.由于置换只有一次反转,因此它是我们试图计算的排列之一.

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


以前的研究

这个问题的一个子问题以前问这里在计算器上.给出了使用合并排序的O(n log n)解决方案,其计算单个排列中的反转次数.但是,如果我使用该解决方案来计算每个排列的反转次数,我仍然会得到O(n!*n log n)的时间复杂度,在我看来这仍然很高.

之前在Stack Overflow上也提到了这个确切的问题,但它没有收到任何答案.


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

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

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

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

Vin*_*uri 12

解决方案需要一些解释.让我们用I(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 |
|         |         |         |         |         |         |         |
Run Code Online (Sandbox Code Playgroud)

现在找到n = 5的排列数,并且对于每个可能的k,我们可以通过在每个排列中的某个位置插入第n个(最大)元素(5)从I(4,k)导出重现I(5,k).先前的排列,以便得到的反转次数为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    |         |         |
|         |         |         |         |         |         |         |
Run Code Online (Sandbox Code Playgroud)

包含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(5,5)来自I(4,k)

|___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    |         |
|         |         |         |         |         |         |         |
Run Code Online (Sandbox Code Playgroud)

所以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;
}
Run Code Online (Sandbox Code Playgroud)


Sha*_*ank 8

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

在查看了关于离散数学反演维基百科页面之后,我在页面底部找到了一个有趣的建议.

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

我点击了OEIS的链接,它向我展示了一个无限的整数序列,称为Mahonian数字的三角形.

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 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和n> 4,它都是有效的(即大于0个排列)答案.

该序列的公式如下:

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

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

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

1
1 1
1 2 2 1
1 3 5 6 5 3 1
etc.
Run Code Online (Sandbox Code Playgroud)

所以基本上计算我的答案归结为简单地计算第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()
Run Code Online (Sandbox Code Playgroud)

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


mcd*_*lla 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反转的排列数.

  • 我没有检查你的答案,但我一点也不感到惊讶。在 http://algo.inria.fr/flajolet/Publications/book.pdf 上有一本可下载的书,它声称教你如何通过生成函数解决大量这样的问题,但我没有找到时间或通过它工作的动力。 (2认同)