算法老师留了三个作业给我们。额,因为他的题目写在黑板上,不能Copy,所以没抄了。不过,按照我意思的理解。他出的题目就是我这篇文章的题目吧。嗯嗯,根据老师的题目,我还是把题目意思写过来。
题目1 全排列
Description
输入正整数n,按字典序从小到大的顺序输出1~n的所有排列。
Input
输入一个正整数n(1<=n<=20)。
Output
输出前n个数的所有序列,序列之间的数字用空格隔开,每个序列占一行。
Sample Input
3
Sample Output
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
题目2 不重复排列
Description
按字典序从小到大不重复的顺序输出n个数的所有排列。
Input
输入的第一行是一个正整数n(1<=n<=20),表示整数的个数。第二行有n个整数,用空格隔开。
Output
输出n个数的所有不重复序列,序列之间的数字用空格隔开,每个序列占一行。
Sample Input
3
8 -8 8
Sample Output
-8 8 8
8 -8 8
8 8 -8
题目3 枚举子集
Description
从集合{1,2,3,...,n}中选取k个数所组成的所有集和。
Input
输入的两个正整数。第一个数为n(1<=n<=20),第二个数为k,(k<=n),两个数之间用空格隔开。
Output
输出含有k个数的所有不相同的集合,输出集合的序列按照字典序输出,每个集合占一行,集合的相邻两个数字用空格隔开。
Sample Input
3 2
Sample Output
1 2
1 3
2 3
嗯嗯,题目意思描述可能和老师描述的有点差异。请结合Sample Input和Sample Output应该就能看懂了。在这篇博文中,老师的题目只是作为引子,可能到最后我不会把这三个题目用Code实现,但我会逐渐把我理解的关于怎样生成全排列和怎样生成子集的知识讲出来。OK,就开始我的全排列和子集生成之旅吧。
===================================我是无奈的分割线===================================
第一次接触全排列是是我大二时候参加ACM的暑假选拔赛遇到的。那个时候我是使用STL中的next_permutation交的。不过很不幸的是TLE(Time Limit Exceeded)了。不是因为next_permutation的效率而TLE,而是我那个时候自以为是的在输入n前加了一个while(true),所以理所当然TLE了。呵呵,偶提这个东西只是想让你们知道,有很多的基本算法都在STL里面实现了。当然,有可能有的同学还不晓得STL为何物,那你就先把她当成是一位PLMM吧。
好的,首先,我要介绍的是使用递归的思想来实现全排列。我们看测试样例的输出就可以看出,在生成全排列的时候,以1开头的的序列,首先输出1,然后输出2~n的序列。而你把1开始的序列的前面的1去掉,则变成了2~n的按照字典序输出的序列,把2开始的序列的前面的2去掉……我相信你对递归了解的话,很显然看到这是一个不断递归的过程。按照这个思想,我们只要把递归的过程写出来,找到递归的出口,全排列自然就出来了。按照这个思想,我就写一个伪代码:
void full_permutation(序列a[], 初始化集合s[]) { if(集合s[]为空) 输出序列a[]; else 按照从小到大的顺序依次考虑集合s[]中的每个元素elem { full_permutation(在序列a[]的末尾加elem元素后得到新序列, 把元素elem从集合s[]中移除); } }
额,不要嫌伪代码有点难看,偶觉得只有能让人很容易明白的伪代码才是好伪码。所以,我找女朋友的标准一直是if(有内涵) 追她;我不敢使用if(漂亮||有内涵) 追她;虽然有可能让我追到一个(漂亮&&有内涵)的MM,但也有可能追到一个(漂亮&&!内涵)的MM。当然,你可能会说为什么不把if里面的判断语句改为(有内涵&&漂亮)呢?额,我没那个资本……话说还是来稍微解释一下那个伪代码吧。递归的边界应该很好理解吧,当集合s[]中没有一个元素的时候,按照上面的伪码,s[]中的元素只能向序列a[]中跑,s[]没了元素,那么序列a[]就是一个完整的序列了。那么,直接输出序列a[]即可。按照从小到大的顺序考虑s[]中的每个元素,每次递归的调用以a[]开始。
如果伪码了解了,那么就得用代码实现了。很容易想到序列a[]用数组保存集合s[]中跑过来得数字,而s[]呢?如果要完成老师布置的第一个题目,那么s[]中的元素根本不要保存,因为s[]中的元素往a[]中跑,那么,跑过来得数字就间接的保存在了序列a[]中,只要没在序列a[]中出现过的数字都可以备选。由于C/C++传递数组的时候传递的是数组的首地址,所以,还需要传一个到底填了多少个数,也就是到底填到数组的第几个位置来了,我们把这个参数取名为cur。OK,下面就用代码来实现老师布置的第一个题目吧。
#include 可能有很多同学对于递归的机制完全不理解,其实我一直也不是很理解……C/C++语言函数,调用自己和调用其他函数没有什么区别,都是建立新栈(这就是为什么递归次数太多容易stack overflow),传递参数并修改相应的数据。在函数执行完毕的时候删除栈,处理返回值并修改相应的数据。打个比较简单的比方,如果我想知道CMM是不是喜欢我(LCQ),但我又不能直接问CMM。于是,就有了下面这个过程: LCQ:AMM,你帮我问下CMM是不是喜欢我; 于是,CMM把不喜欢LCQ这个结果给BMM,BMM又把这个结果返回AMM,AMM又把这个不幸的结果告诉LCQ。于是,LCQ终于知道了CMM不喜欢他……这就是一个递归的实例。当然,这个狗屁比方甚是不好,但是递归基本就是这样一个过程,不断的给下一个被递归的函数安排工作。要是你还是没明白递归是啥回事,下面我在word上画了一个当n==3时怎样生成全排列的过程,其中x表示的是未被下一级递归函数不确认的数字。图画的不好,凑合着看吧。 好的,递归生成全排列就到此结束。如果你还是不怎么理解递归,那就用我在网上看到的另外一种方法实现,他的名字叫做字典序生成算法。 字典序生成算法: 例如839647521是数字1~9的一个排列。从它生成下一个排列的步骤如下: 我只是按照这个算法的思想写了一个Code。这个算法的思想我是经过查阅网上的blog得到的。在写这篇blog的过程中,我参考了很多的文章,我现在不一一列举,等写完这篇博客之后,我会把他们的超链接全部放到后面,有些看不懂的东西你们点击超链接去看。比如这个算法为什么这样就能得到下一个全排列,我就不把证明Copy到这个上面了,下面是我按照这个算法写的Code。代码有点繁琐,命名的地方可能有点不规范,请大家凑合看一下,你们自己写代码的时候可以参考一下。
AMM:BMM,你帮我问下CMM是不是喜欢LCQ;
BMM:CMM,你喜不喜欢LCQ;
CMM:经过我用KMP匹配之后,我的表白中找不到"LCQ"这个子串。
设 P 是 1 ~ n 的一个全排列 :p = p1 p2 ...pn = p1 p2 ...pj-1 pj pj+1 ......pk-1 pk pk+1 ...pn
1、从排列的右端开始,找出第一个比右边数字小的数字的序号j ( j 从左端开始计算),即 j = max{i | pi < pi+1 }
2、在pj 的右边的数字中,找出所有比 pj 大的数中最小的数字 pk ,即 k = max{i | pj < pi } 右边的数从右至左是递增的,因此k是所有大于pj的数字中序号最大者)
3、对换pj,pk
4、再将pj+1 ......pk-1 pk pk+1 ...pn 倒转得到排列 p' = p1 p2 ...pj-1 pj pn ......pk+1 pk pk-1 ...pj+1 , 这就是排列p的下一个排列。
1、自右至左找出排列中第一个比右边数字小的数字4;
2、在该数字后的数字中找出比4大的数中最小的一个5;
3、将5与4交换得到839657421;
4、将7421倒转得到839651247;
5、所以839647521的下一个排列是839651247。