全排列生成算法(三)
2011-12-15 16:00
253 查看
基于阶乘数的全排列生成算法,是另一种通过序列顺序,输出全排列的算法。所谓阶乘数,实际上和我们常用的2进制,8进制,10进制,16进制一样,是一种数值的表示形式,所不同的是,上面这几种进制数,相邻位之间的进制是固定值,以10进制为例,第n位与第n+1位之间的进制是10,而阶乘数,相邻两位之间的进制是变值,第n位与第n+1位之间的进制是(n+1)!。对于10进制数,每一位的取值范围也是固定的0~9,而阶乘数每一位的取值范围为0~n。可以证明,任何一个数量,都可以由一个阶乘数唯一表示。下面以23为例,说明其在各种进制中的表现形式
其中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
(最低位从第0位开始),如果F
+1>n,那么我们需要将F
置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排列的对应关系如下表所示。
由此,我们就可以利用阶乘数与排列的对应关系构建集合的全排列,算法如下。
对于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,排列生成结束。整个算法代码如下
从生成排列顺序的角度讲,概算法相较于字典序和最小变更有明显优势,但是在实际应用中,由于根据阶乘数所定义的逆序构建排列是一个O(n^2)时间复杂度的过程,所以算法的整体执行效率逊色不少。但是通过阶乘数建立逆序数与排列对应关系的思路,还是十分精彩的,值得借鉴。
2进制 | 8进制 | 10进制 | 16进制 | 阶乘数 | |
23 | 10111 | 27 | 23 | 17 | 3210 |
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
(最低位从第0位开始),如果F
+1>n,那么我们需要将F
置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,排列生成结束。整个算法代码如下
inline int FacNumNext(unsigned int* facnum, size_t array_size) { unsigned int i = 0; while(i < array_size) { if(facnum[i] + 1 <= i) { facnum[i] += 1; return 0; } else { facnum[i] = 0; ++i; } } return 1; } /* * 根据阶乘数所指定的逆序数根据原始字符串构建排列输出 */ inline void BuildPerm(const char* array, size_t array_size, const unsigned int* facnum, char* out) { char t; unsigned int i, j; memcpy(out, array, array_size * sizeof(char)); for(i = 0; i < array_size - 1; ++i) { j = facnum[array_size - 1 - i]; if(j != 0) { t = out[i + j]; memmove(out + i + 1, out + i, j * sizeof(char)); out[i] = t; } } } /* * 基于阶乘数(逆序数)的全排列生成算法 */ void FullArray(char* array, size_t array_size) { unsigned int facnum[array_size]; char out[array_size]; for(unsigned int i = 0; i < array_size; ++i) { facnum[i] = 0; } BuildPerm(array, array_size, facnum, out); for(unsigned int i = 0; i < array_size; ++i) { cout << out[i] << ' '; } cout << '\n'; while(!FacNumNext(facnum, array_size)) { BuildPerm(array, array_size, facnum, out); for(unsigned int i = 0; i < array_size; ++i) { cout << out[i] << ' '; } cout << '\n'; } }用该算法生成1234全排列,顺序如下图,该图来自与Wiki百科。
从生成排列顺序的角度讲,概算法相较于字典序和最小变更有明显优势,但是在实际应用中,由于根据阶乘数所定义的逆序构建排列是一个O(n^2)时间复杂度的过程,所以算法的整体执行效率逊色不少。但是通过阶乘数建立逆序数与排列对应关系的思路,还是十分精彩的,值得借鉴。
相关文章推荐
- 全排列的生成算法
- 全排列生成的迭代算法
- 全排列生成算法与组合数排列求法
- 全排列生成算法(二)
- 全排列生成算法
- 全排列生成算法(三)
- 全排列生成算法:next_permutation
- 生成全排列----std::next_permutation 算法解析
- 全排列生成算法之字典序
- 【STL】全排列生成算法:next_permutation
- 全排列的生成算法
- 全排列的四种生成算法
- 字符串全排列生成算法
- 全排列的生成算法:字典序法
- 算法学习笔记(三)——全排列生成算法:next_permutation
- 【算法系列学习一】全排列的生成算法
- 全排列生成算法笔记
- 全排列的生成算法 字典序法
- 算法谜题90 座位重排(生成全排列的Johnson–Trotter算法)
- n个数全排列的非递归生成算法,C实现