【摘录】《程序设计导引及在线实践》之排列

问题描述
大家知道,给出正整数n,则1 到n 这n 个数可以构成n!种排列,把这些排列按照从
小到大的顺序(字典顺序)列出,如n=3 时,列出1 2 3,1 3 2,2 1 3,2 3 1,3 1 2,3 2 1
六个排列。
给出某个排列,求出这个排列的下k 个排列,如果遇到最后一个排列,则下1 排列为第
1 个排列,即排列1 2 3…n。
比如:n = 3,k=2 给出排列2 3 1,则它的下1 个排列为3 1 2,下2 个排列为3 2 1,因
此答案为3 2 1。
输入数据
第一行是一个正整数 m,表示测试数据的个数,下面是m 组测试数据,每组测试数据
第一行是2 个正整数n( 1 <= n < 1024 )和k(1<=k<=64),第二行有n 个正整数,是1,2 … n
的一个排列。
输出要求
对于每组输入数据,输出一行,n 个数,中间用空格隔开,表示输入排列的下k 个排列。
输入样例
3
3 1
2 3 1
3 1
3 2 1
10 2
1 2 3 4 5 6 7 8 9 10
输出样例
3 1 2
1 2 3
1 2 3 4 5 6 7 9 8 10
解题思路
这道题目,最直观的想法是求出 1 到n 的所有排列,然后将全部排列排序……且慢,n
最大可以是1024,1024! 个排列,几乎永远也算不出来,算出来也没有地方存放。那么,
有没有公式或规律,能够很快由一个排列推算出下k 个排列呢?实际上寻找规律或公式都是
徒劳的,只能老老实实由给定排列算出下一个排列,再算出下一个排列……一直算到第k
的排列。鉴于k 的值很小,最多只有64,因此这种算法应该是可行的。
如何由给定排列求下一个排列?不妨自己动手做一下。比如:
“2 1 4 7 6 3 5”的下一个排列是什么?容易,显然是“2 1 4 7 6 5 3”,那么,再下一个
排列是什么?有点难了,是“2 1 5 3 4 6 7”。
以从“2 1 4 7 6 5 3”求出下一个排列 “2 1 5 3 4 6 7”作为例子,可以总结出求给定排
列的下一个排列的步骤:
假设给定排列中的n 个数从左到右是a1, a2, a3……an 。
1) 从 an 开始,往左边找,直到找到某个aj,满足aj-1 < aj(对上例, 这个aj 就是 7,
aj-1 就是4)。
2) 在 aj 、aj+1…… an 中找到最小的比aj-1 大的数,将这个数和 aj-1 互换位置(对
上例, 这个数就是5,和4 换完位置后的排列是 “2 1 5 7 6 4 3”)。
3) 将从位置 j 到位置n 的所有数(共n-j+1 个)从小到大重新排序,排好序后,
新的排列就是所要求的排列。(对上例,就是将“7 6 4 3”排序,排好后的新排列就是“2
1 5 3 4 6 7”)。
当然,按照题目要求,如果a1, a2, a3……an 已经是降序,那么它的下一个排序就是an, an-1,
an-2……a1 。
参考程序:
1. #include <stdio.h>
2. #include <stdlib.h>
3. #define MAX_NUM 1024
4. int an[MAX_NUM + 10];
5. //用以排序的比较函数
6. int MyCompare( const void * e1, const void * e2)
7. {
8. return * ((int *) e1) - * ((int *) e2);
9. }
10.
11. main()
12. {
13. int M;
14. int n, k, i, j;
15. scanf("%d", & M);
16. for (int m = 0; m < M; m ++ ) {
17. scanf("%d%d", &n, &k);
18. //排列存放在 an[1] .... an[n]
19. for( i = 1; i <= n; i ++ )
20. scanf("%d", &an[i]);
21. an[0] = 100000; //确保an[0]比排列中所有的数都大
22. for( i = 0; i < k ;i ++ ) { //每次循环都找出下一个排列
23. for( j = n ; j >= 1 && an[j-1] > an[j] ; j -- ) ;
24. if( j >= 1 ) {
25. int nMinLarger = an[j];
26. int nMinIdx = j;
27. //下面找出从an[j]及其后最小的比 an[j-1]大的元素,并记住其下标
28. for( int kk = j; kk <= n; kk ++)
29. if( nMinLarger > an[kk] && an[kk] > an[j-1]) {
30. nMinLarger = an[kk];
31. nMinIdx = kk;
32. }
33. //交换位置
34. an[nMinIdx] = an[j-1];
35. an[j-1] = nMinLarger;
36. qsort( an + j, n - j + 1 , sizeof(int), MyCompare); //排序
37. }
38. else { //an 里的排列已经是降序了,那么下一个排列就是 1 2 3 。。。。 n
39. for( j = 1; j <= n; j ++ )
140
40. an[j] = j;
41. }
42. }
43. for( j = 1; j <= n; j ++ )
44. printf("%d ", an[j]);
45. printf("\n");
46.
47. }
48. }
语句36 是对一个数组的局部进行排序。qsort 函数并不要求第一个参数必须是一个数组
的开始地址,只要是待排序的一片连续空间的开始地址即可。同样,qsort 的第二个参数也
不必一定是整个数组的元素个数,只要是待排序的元素个数即可。
实现技巧
1.把排列存放在an[1] .... an[n],而在an[0]存放一个比排列中所有的数都大的数,这个
an[0]所起的作用通常称之为“哨兵”。有了“哨兵”,就可以写语句23:
for( j = n ; j >= 1 && an[j-1] > an[j] ; j -- ) ;
而23 不必担心j-1 小于0 导致数组越界。如果没有“哨兵”,而且将排列存放在an[0] ....
an[n-1]中,那么写到相当于语句23 的这个for 循环的时候,就要判断j-1 小于0 的情况,比
较罗嗦,也容易出错。
放置“哨兵”,是在数组或链表中进行各种操作时常用的做法。
2. 学过C++标准模板库的同学会注意到,用标准模板库中的next_permutation 算法直
接就能求给定排列的下一个排列,根本不需动脑筋。
常见问题
这个题目的测试数据比较多,用 scanf 读入没有问题,有的同学学了点C++, 用C++中
的cin 读入数据,就会造成超时。

posted @ 2010-09-17 21:46  skywter  阅读(289)  评论(0)    收藏  举报