烧饼的技术乱讲
In: Algorithm
23 三 2010一直以来我对数的排列这类算法问题都感到很头痛,曾经囫囵吞枣看过一些算法,可是浅尝辄止,没有实际操作所以每次都忘记。上周三算法分析与设计课的内容是递归,老师正好提到了排列数这一问题,于是就有机会稍微进行一些研究。
一个排列(permutation)就是对一组对象或者数值按照某种特定的方式进行排列组织(ar-rangement),为了方便起见我们以一个数集{1, 2, 3}为例,它的全排列共有3!=6种,即{1, 2, 3}, {1, 3, 2}, {2, 1, 3}, {2, 3, 1}, {3, 1, 2}, {3, 2, 1},上述列举排列的顺序又称为按字典序列举排列,具体将在下文进行叙述。
在Wikipedia中,字典序的解释是这样的:
In mathematics, the lexicographic or lexicographical order, (also known as dictionary order, alphabetic order or lexicographic(al) product), is a natural order structure of the Cartesian product of two ordered sets.
如果你拥有一个字典,那么里面所有的字符串都是按照字典序来进行排序的,假设有两个字符串:
a1, a2, …, an
以及
b1, b2, …, bn
若存在一个m>0,使得对于所有的i<m有ai=bi,并且am<bm(i>m时ai与bi的关系不用考虑),那么称ap, 1≤p≤n的字典序小于bq, 1≤q≤n。由这个定义可以很容易的发现这样一个事实,当同样长度的一串数值、字符串需要按照字典序排列时,进行比较的位数i总是从n递减至1,例如:1234<1243, 1234<1342, 1243<1342,所以这三个串的顺序是:1234<1243
<1342,比较的位数依次是第3位和第2位;而从高位降低一位进行比较的条件则是对于此时的i,有ai+1≥ai+2≥…≥ai+n,例如当i=2时,12345<12354<12534<12543,此时a3≥a4≥a5,于是接下来将把2替换成字典中下一个数字3来进行排列,这个特点将在本文的最后一个方法中得到应用。
下面就让我们来看一个具体问题,给定一串数字:{a1, a2, …, an},它们两两之间并不重复,要求输出由这些数字组成的全排列。乍看之下似乎可以用穷举法解决,由于n个数字的全排列个数是n!,故而穷举法的时间复杂度为O(n)。那么利用什么数据结构来穷举呢,由于全排列我们需要考虑每一位的各种情况,确定了一位的数字以后就继续试验下一位……也就是说当确定一种可能性之后就会沿着这条路径一直往下,直到确定最后一位数字,这个行为和深度优先搜索算法比较相似,因此我们可以用树来表示穷举的全过程,下图以求{1,2,3}的全排列为例如此方法进行说明:
图一 利用递归生成{1,2,3}的全排列
从上图我们可以看到,每一个排列就是从根结点到叶子结点的一次遍历,其伪代码如下:
上述方法很直观,可是存在着一个问题,那就是它无法判断出某些数是否重复,它只是很单纯的穷举着所有可能出现的排列,例如1,2,2,4这四个数实际排列数为:4!/2!=12种,而上述算法仍然会得出24个结果,它无法区分1,2,2,4和1,2,2,4是一个重复的排列,应该只列举一次。可是递归的方法又非常直观非常方便啊,应该怎么办呢?其实只要在原方法上做一点小改动,人为的多设置一个集合,该集合包含了原集合中所有不重复的数字,从而树同一级的分支用该集合中的数字做为标签进行搜索,这样能够避免同一级出现重复计算的情况,下面以1,2,2为例说明上述方法的错误以本方法的改进:
改进后的伪代码:
从代码中可以看出改进方法只是用一个不包含有重复元素的集合来代替原集合,并用新的集体进行迭代,转化成树形结构如下图所示:
听老师上课的时候说这是一道微软来我们学校面试的考题,其实高爷爷在TAOCP第四卷分册2用了整个分册的内容讨论了如何生成全n元组(n-tuples)和全排列的问题,书中所述的第一个按字典序生成全排列的方法即可以在不必考虑给出的数集中是否有重复数字的情况下完美的生成全排列:
Algorithm L是一个很经典的算法,它简单、灵活,对于给定的输入a1, a2, …, an(要求该输入必须是排好序的,即a1 ≤ a2≤ … ≤ an)可以生成该集合的全排列,按字典序输出,并且可以很好的处理给定的输入中有重复数字的情况。注意到这个算法对于输入的要求必须是有序的,而前面两个算法无论输入的顺序是递增、递减、乱序都可以很好的工作。
这个算法还有一个特点就是它的输出将排列按照字典序输出,那么在设计算法的时候肯定会需要用到字典序的一些特点,让我们来回顾一下上文所述——字典序的比较是从最高位开始逐渐递减至最低位,每次递减的时机是从该位开始至最高位,所有的数字呈从大到小的顺序排列,明白了这个特点就能够很轻松的理解L算法。
让我们先来看一下L算法的思路:
L算法基本上按照思路,共分成4步
L1. [输出] 输出排列a1…an;
L2. [找到j] 令j←n-1,如果aj≥aj+1则将j自减1,直到aj<aj+1。这步的作用就是找到使从j位至最高位所有的数字呈从大到小的顺序排列的这个j,该位就是下一步需要改变(增加)的值。
L3. [更改aj] 令l←n,如果aj≥al则将l自减1,直到出现aj<al,(updated 此时交换aj←→al)
。这步的作用就是找到从第j位至最高位中,大于aj的最小数值,该数值就是下一个字典序中aj的值。
L4. [反转aj...an] 令k←j+1,l←n,如果k<l,交换ak←→al,自增k,自减l,再重复此交换直到k≥l。这步的作用是使由a1…aj开头的排列恢复到字典序的最小位置。
这种文字的步骤似乎比较难以理解,下面一个例子对算法的步骤加以说明:
(1) 整个算法的核心就是找到需要改变的j值,例如13287651,注意到最后四位已经按照从大到小的顺序排列,也就是说13287651是处于132xxxxx这个排列在字典中的最大值,那么下一个排列就要通过修改第3位的2获得。——这是L2.步骤解决的问题
(2) 那么这个2要替换成什么数值才行呢,在132之后一共有五个数字,1,5,6,7,8,让我们来比较它们的大小:13187652<13287651<13587621<13687521<13782651<13827651,可以看到,在字典中紧跟着13287651的排列是13587621,也就是将要替换的2与后面所有数值中大于2的最小数字进行代换。——这是L3.步骤解决的问题
(3) 在替换了2之后,排列变为了13587621,注意到在寻找j的时候我们已经保证了最后五位是按从大到小的顺序排列的,又由于替换的数字是大于aj所有数字当中最小的,所以在替换之后我们仍可以得到一个有序的数列,这个数列是135xxxxx这个形式的所有排列中最大的,于是我们要对它进行反转,这样得到的数列顺序为从小到大,同时也是所有排列中最小的。——这是L4.步骤解决的问题。
之前看到有关求全排列的问题时我也常常看到介绍的就是这个通用算法,可惜当时一直没有认真去琢磨其中每步的作用,所以一直对这个问题敬而远之,在高爷爷的书中还有介绍更多更详细的算法,出于敬畏我有时间了再研读它吧。
1. Permutation (wikipedia):
http://en.wikipedia.org/wiki/Permutation
2. Lexicographical order (wikipeida):
http://en.wikipedia.org/wiki/Lexicographic_ordering
3. Donald Knuth. The Art of Computer Programming, Volume 4: Generating All Tuples and Permutations, Fascicle 2, first printing. Addison-Wesley, 2005. ISBN 0-201-85393-0.
I'm now a graduate student of Computer Applied Technology in Tongji University. I like Computer Graphics, Web 2.0, Magic, Music and am partially a geek. This blog is about C++, algorithm, cg, comments and other things I may get in touch with in the near future. Hope everyone enjoy this little site. Contact me: 4everlove.xu AT gmail.com
5 Responses to 生成n个数的全排列(Permutation)
bcxx
五月 17th, 2010 at 13:52
不错哈,不过网站就有点~
TopCoder « 人云亦云
八月 28th, 2010 at 15:07
[...] 生成n个数的全排列(Permutation) [...]
dragonkid
二月 7th, 2011 at 19:25
为什么图片都没了呢?
烧饼
二月 8th, 2011 at 20:20
@dragonkid
不好意思啊我前一段迁移主机,然后图片的地址变了一直没有去弄,现在更新一下
harris
十月 14th, 2011 at 22:33
Permutation(A,S,i)
1 if Len[S]=0 Then Print(A)
2 for each s in S
3 A[i]=s
4 call Permutation(A,S-{s},i+1)
5 repeat
6 end Permutation
这里A是什么啊,i是什么啊