排列算法 Permutation Generation
一直以为排列是蛮基本的算法,没什么好研究的。因为有个很简单的算法,我们可以递推生成所需排列。
比如已经知道两个元素的排列,a1 a2, a2 a1,引入第三个元素后,所需做的就是往前面结果里insert。
a3 a1 a2, a1 a3 a2, a1 a2 a3 ... 这种定义显然是递归的,所以就会认为很容易转换出清晰的程序了。
以前甚至还颇得意的用ruby写过这样自我感觉很好的程序。现在回想真是汗颜,多么弱智且没效率啊。
主要问题在于,如何在每下一次的生成排列做到,不用额外空间,最好是原地的,并且修改量要小,否则就是一个大常数乘以恐怖的N!。
看来轻视学问总是要被报复的,知耻而后勇,花了点时间学习一下前辈们对于排列的研究吧。
没想到简单的搜了几篇,排列竟有这么多的研究和应用。大牛Donald Knuth在The art of computer programming 最新第四卷专门为排列开设课题这里,面对这种符号流的圣经,我只能蜻蜓点水,学点皮毛了。
这篇Princeton的slides作为入门还是不错的。主要的算法(其实都很古老了,最早可以追溯到1650s)包括1. backtracking 2. Plain changes 3.字典序列(lexicographic genenration)4. Index table 5.Heap's algorithm
下面介绍一下主要的算法思想 和 简单的实现
1. 字典序列
这个方法的思路在于按照从“小到大”的方式生成所有排列。假设每一个参与排列的项都是可以比较大小的,即使不很直观也总可以找到某种映射法(随便赋个数),字典序就是最左的位权重最高,向右依次比较大小。
1. 算法的初始步骤为,排成最小的序列。a1<a2<...<an (为了方便打字 假设每项唯一)
2. 然后从右向左,找到第一个 aj<aj+1,意味着aj+1>aj+2>...>an
3. 在[aj+1到an]中找到最小的ak并且aj<ak ,交换aj和ak
4. 镜像反转[aj+1到an]的元素,重复第二步直到序列最大,a1>a2>...>an
以{1,2,3}为例子,所产生的序列为
1,2,3
1,3,2
2,1,3
2,3,1
3,1,2
3,2,1
给人的感觉是和十进制进位非常相像。都是从右边最低位开始变化,每次向左移动,为了尽可能小的增大,相当于十进制的加一。每次交换反转相当于进位,也是要最小化增大量,被交换最小的ak是aj最合适的下一位,因为选择其他的都会造成不连续。反转使极大变成极小,相当于从9到0的回归。算法的效率不会太好,因为存在大量的反转操作。
2. backtracking
相当直观好懂的算法,直接给出实现
[a1,a2 ... aN]
generate(int N)
{
if (N == 1) dosomething();
for (int i = 1; i <= N; i++)
{ swap(i, N); generate(N-1); swap(i, N); }
}
假设generate(N-1)能产生所有排列,那么通过和之前N-1个元素交换最后的第N个元素(或者不交换)可以产生新的排列,所产生的个数正好是N×generate(N-1),满足排列定义,所以是N!个。算法中的后一个swap是为了保证把最后一个元素换回原来位子,还原整个序列,注意所有递归操作施加在了同一个静态数组上。
3. Plain changes
或者叫做 Johnson-Trotter 算法。令人佩服的是这个算法可以追溯到17世纪的英国。当时的教堂有很多编钟,为了每次摆出不同的花样(为了my lord),需要对钟的排列作出调整。古人不容易啊要想算出5个钟120种变化已经很不容易了,而且更令人抓狂的是,这些编钟各个体形巨大、笨重,所以需要一种每次移动最少的方法。至理名言,最优秀的程序员是因为太懒了,为了“偷懒”ringers找到了好办法,后来被形式化成这个算法了(这里有相关历史介绍)
设[a1,a2 ... aN] 每一项都有向左或向右两个移动方向。
1.初始化所有移动方向向左
2.如果移动方向的值比自己小,就可移动,比如 <1 >2 <3, 3可以移动,2不可以因为3大
3.移动最大的可以动项
4.将所有比移动项大的项方向反转 重复第三步 直到不能移动为止
举个N=4的例子:
<1 <2 <3 <4
<1 <2 <4 <3
<1 <4 <2 <3
<4 <1 <2 <3
4> <1 <3 <2
<1 4> <3 <2
<1 <3 4> <2
<1 <3 <2 4>
<3 <1 <2 <4
<3 <1 <4 <2
<3 <4 <1 <2
<4 <3 <1 <2
4> 3> <2 <1
3> 4> <2 <1
3> <2 4> <1
3> <2 <1 4>
<2 3> <1 <4
<2 3> <4 <1
<2 <4 3> <1
<4 <2 3> <1
4> <2 <1 3>
<2 4> <1 3>
<2 <1 4> 3>
<2 <1 3> 4>
1. Heap's algorithm
是对backtracking的改进,减少了第一次swap,效率渴望翻倍
[a1,a2 ... aN]
{
if (N == 1) dosomething();
for (int i = 1; i <= N; i++)
{ generate(N-1); swap(N % 2 ? 1 : i, N); }
}
如果下标从零开始swap改为N % 2 ? i : 0或者N % 2 * i
该算法和index table有关,而这个方法又牵涉到cycle index是和group theory有关的东西了。黑茫茫的隧道一样的知识链条啊。何日能一观瞻要看缘分了。