全排列生成算法(三)

基于阶乘数的全排列生成算法,是另一种通过序列顺序,输出全排列的算法。所谓阶乘数,实际上和我们常用的2进制,8进制,10进制,16进制一样,是一种数值的表示形式,所不同的是,上面这几种进制数,相邻位之间的进制是固定值,以10进制为例,第n位与第n+1位之间的进制是10,而阶乘数,相邻两位之间的进制是变值,第n位与第n+1位之间的进制是(n+1)!。对于10进制数,每一位的取值范围也是固定的0~9,而阶乘数每一位的取值范围为0~n。可以证明,任何一个数量,都可以由一个阶乘数唯一表示。下面以23为例,说明其在各种进制中的表现形式

  2进制 8进制 10进制 16进制 阶乘数
23 10111 27 23 17 3210

 

其中10进制23所代表的数量的计算方法为

D(23) = 2×10^1 + 3×10^0 = 2×10 + 3×1 = 23

阶乘数3210所代表的数量的计算方法为

F(3210) = 3×3! + 2×2! + 1×1! + 0×0! = 3×6 + 2×2 + 1×1 + 1×0 = 23

对于阶乘数而言,由于阶乘的增长速度非常快,所以其可以表示的数值的范围随着位数的增长十分迅速,对于n位的阶乘数而言,其表示的范围从0~(n+1)!-1,总共(n+1)!个数。阶乘数有很多性质这里我们只介绍其和全排列相关的一些性质。

首先是加法操作,与普通十进制数的加法基本一样,所不同的是对于第n位F[n](最低位从第0位开始),如果F[n]+1>n,那么我们需要将F[n]置0,同时令F[n+1]+1,如果对于第n+1位,也导致进位,则向高位依次执行进位操作。这里我们看一下F(3210)+1,对于第0位,有F[0]+1=0+1=1>0,所以F[0]=0(实际上阶乘数的第0位一直是0),F[1]+1=1+1=2>1,F[1]=0,……,依次执行,各位都发生进位,最终结果F(3210)+1=F(10000)。

其次,对于n位的阶乘数,每一个阶乘数的各位的数值,正好对应了一个n排列各位的逆序关系。这里以abcd为例。例如F(2110),其对应的排列的意思是,对于排列的第一个元素,其后有两个元素比他小;第二个元素,后面有一个元素比他小;第三个元素,后面有一个元素比他小。最终根据F(2110)构建的排列为cbda。4位的阶乘数,与4排列的对应关系如下表所示。

0000 abcd 1000 bacd 2000 cabd 3000 dabc
0010 abdc 1010 badc 2010 cadb 3010 dacb
0100 acbd 1100 bcad 2100 cbad 3100 dbac
0110 acdb 1110 bcda 2110 cbda 3110 dbca
0200 adbc 1200 bdac 2200 cdab 3200 dcab
0210 adcb 1210 bdca 2210 cdba 3210 dcba

 

由此,我们就可以利用阶乘数与排列的对应关系构建集合的全排列,算法如下。

  • 对于n个元素的全排列,首先生成n位的阶乘数F[0...n-1],并令F[0...n-1]=0。
  • 每次对F[0...n-1]执行+1操作,所得结果,根据其与排列的逆序对应关系,生成排列。
  • 直到到达F[0...n-1]所能表示的最大数量n!-1为止,全部n!个排列生成完毕。

这里有一个问题需要解决,就是如何根据阶乘数,及其与排列逆序的对应关系生成对应的排列,这里给出一个方法,

  • 以字典序最小的排列a[0...n-1]作为起始,令i从0到n-2。
  • 如果F[i]=0,递增i。
  • 否则令t=a[i+F[i]],同时将a[i...i+F[i]-1]区间的元素,向后移动一位,然后令a[i]=t,递增i。

下面说明一下如何根据阶乘数F(2110)和初始排列abcd,构建对应的排列。首先,我们发现F[0]=2,所以我们要将a[0+2]位置的元素c放在a[0]位置,之前,先用临时变量t记录a[2]的值,然后将a[0...0+2-1]区间内的元素向后移动一位,然后令a[0]=t,得到cabd,i值增加1;然后有F[1]=1,所以我们要将a[1+1]=a[2]=b放在a[1]位置,同时将a[1]向后移动一位,得到排列cbad;然后有F[2]=1,所以将a[2+1]=a[3]=d放在a[2]位置,同时a[2]向后移动一位。最终得到cbda,排列生成结束。整个算法代码如下

 1 inline int FacNumNext(unsigned int* facnum, size_t array_size)
 2 {
 3     unsigned int i = 0;
 4 
 5     while(i < array_size)
 6     {
 7         if(facnum[i] + 1 <= i)
 8         {
 9             facnum[i] += 1;
10             return 0;
11         }
12         else
13         {
14             facnum[i] = 0;
15             ++i;
16         }
17     }
18 
19     return 1;
20 }
21 
22 /*
23  * 根据阶乘数所指定的逆序数根据原始字符串构建排列输出
24  */
25 inline void BuildPerm(const char* array, size_t array_size, const unsigned int* facnum, char* out)
26 {
27     char t;
28     unsigned int i, j;
29 
30     memcpy(out, array, array_size * sizeof(char));
31 
32     for(i = 0; i < array_size - 1; ++i)
33     {
34         j = facnum[array_size - 1 - i];
35 
36         if(j != 0)
37         {
38             t = out[i + j];
39             memmove(out + i + 1, out + i, j * sizeof(char));
40             out[i] = t;
41         }
42     }
43 }
44 
45 /*
46  * 基于阶乘数(逆序数)的全排列生成算法
47  */
48 void FullArray(char* array, size_t array_size)
49 {
50     unsigned int facnum[array_size];
51     char out[array_size];
52 
53     for(unsigned int i = 0; i < array_size; ++i)
54     {
55         facnum[i] = 0;
56     }
57 
58     BuildPerm(array, array_size, facnum, out);
59 
60     for(unsigned int i = 0; i < array_size; ++i)
61     {
62         cout << out[i] << ' ';
63     }
64 
65     cout << '\n';
66 
67     while(!FacNumNext(facnum, array_size))
68     {
69         BuildPerm(array, array_size, facnum, out);
70 
71         for(unsigned int i = 0; i < array_size; ++i)
72         {
73             cout << out[i] << ' ';
74         }
75 
76         cout << '\n';
77     }
78 }

用该算法生成1234全排列,顺序如下图,该图来自与Wiki百科。

从生成排列顺序的角度讲,概算法相较于字典序和最小变更有明显优势,但是在实际应用中,由于根据阶乘数所定义的逆序构建排列是一个O(n^2)时间复杂度的过程,所以算法的整体执行效率逊色不少。但是通过阶乘数建立逆序数与排列对应关系的思路,还是十分精彩的,值得借鉴。

posted @ 2014-03-18 11:25  玩笑528  阅读(745)  评论(0编辑  收藏  举报