我正在努力有效地解决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来回答下面的问题。
答案 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)。