排列算法 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]

 generate(int N)
 {
      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有关的东西了。黑茫茫的隧道一样的知识链条啊。何日能一观瞻要看缘分了。

 

posted @ 2010-01-21 22:07  dragonpig  阅读(3636)  评论(0编辑  收藏  举报