天天看点

排列与组合的算法实现

由于老师提出了全排列问题。我心血来潮,将组合也弄了一下。 现在写篇日志记录一下。

全排列

目前我所知道的全排列算法有三种,下面一一介绍:

(1)分治算法:这个算法利用了分而治之的思想。我们先从2个数开始,比如说4,5,他们的全排列只有两个45和54。如果在前面加个3,那么全排列就是345,354,也就是3(54),括号表示里面的数的全排列。当然还有4(35),5(34)...写到这里,各位看官应该已经看出点门道了吧。三个数的全排列,可以分为三次计算,第一次计算3和(45)的全排列,第二次计算4和(35)的全排列.....也就是说,将序列第一个元素分别与它以及其后的每一个元素代换,得到三个序列,然后对这些序列的除首元素外的子序列进行全排列。思想其实还是挺简单的:

代码实现如下:

Code:

  1. //input 8 12.61s consumed  
  2. //input 8 11.72s consumed remove '-' in the printed array  
  3. #include <stdio.h>  
  4. #include <string.h>  
  5. #include <stdlib.h>  
  6. #define LENGTH 27  
  7. int n=0;  
  8. void permute(int[],int,int);  
  9. void swapint(int &a,int &b);  
  10. void printIntArray (int[],int);  
  11. //Function Implementation  
  12. void swapint(int &a,int &b){  
  13.     int temp;  
  14.     temp = a;  
  15.     a = b;  
  16.     b = temp;  
  17. }  
  18. void printIntArray(int target[],int length){  
  19.     int i;  
  20.     for (i=0;i<length;i++) printf ("%d",target[i]);  
  21.     printf ("/n");  
  22. }  
  23. void permute(int target[],int begin,int end){  
  24.     if (begin==end) {  
  25.         printIntArray(target,end+1);  
  26.         n++;  
  27.         return;  
  28.     }  
  29.     int i;  
  30.     for (i=begin;i<=end;i++){  
  31.         swapint(target[begin],target[i]);  
  32.         permute(target,begin+1,end);  
  33.         swapint(target[begin],target[i]);  
  34.     }  
  35. }  
  36. //test Functions  
  37. void testPermute(){  
  38.     int len;  
  39.     scanf ("%d",&len);  
  40.     int *testcase =(int *)malloc(len*sizeof(int));  
  41.     int i;  
  42.     for (i=0;i<len;i++) testcase[i]=i+1;  
  43.     permute(testcase,0,len-1);  
  44. }  
  45. //Main Function  
  46. void main(){  
  47.     testPermute();  
  48.     printf ("n=%d",n);  

需要注意的是交换了元素,然后进行递归对子序列进行全排列之后,需要将元素的位置换回来。这是为了要还原现场,也就是说递归函数permute的形参target数组在内存中只有一个,递归调用的时候,入栈的只是一个数组指针,递归对子序列进行全排列之后,必定会对原序列造成一定的乱序。因此每次递归之后,都需要还原现场。

当然还有一个方法是将数组放在一个结构体中,而且还不能使用指针哦。参数调用的时候也不能使用引用。这样递归的时候,内存中才会生成新的结构体,当然也有新的数组了。就不会影响其他的操作,不过可想而知,这样写多浪费内存空间,当然也浪费时间。

(2)字典序列算法

字典序列算法是一种非递归算法。而它正是STL中Next_permutation的实现算法。我们来看看他的思路吧:

它的整体思想是让排列成为可递推的数列,也就是说从前一状态的排列,可以推出一种新的状态,直到最终状态。比如说,最初状态是12345,最终状态是54321。其实我觉得这跟我们手动做全排列是一样的。首先是12345,然后12354,然后12435,12453....逐渐地从后往前递增。

看看算法描述:

首先,将待排序列变成有序(升序)序列。

然后,从后向前寻找,找到相邻的两个元素,Ti<Tj,(j=i+1)。如果没有找到,则说明整个序列已经是降序排列了,也就是说到达最终状态54321了。此时,全排列结束。

接着,如果没有结束,从后向前找到第一个元素Tk,使得Tk>Ti(很多时候k=j),找到它,交换Ti跟Tk,并且将Tj到Tn(Tn是最后一个元素)的子序列进行倒置操作。输出此序列。并回到第二步继续寻找ij.

算法是很清晰的。看看代码:

Code:

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. //function definition  
  4. void Swap(int &a,int &b);  
  5. void Reverse (int[],int,int);  
  6. void printArray (int[],int);  
  7. int nextPermutation(int[],int);  
  8. void Swap(int &a,int &b){  
  9.     int tmp;  
  10.     tmp=a;  
  11.     a=b;  
  12.     b=tmp;  
  13. }  
  14. void Reverse(int target[],int begin,int end){  
  15.     while (begin<end){  
  16.         Swap(target[begin],target[end]);  
  17.         begin++;  
  18.         end--;  
  19.     }  
  20. }  
  21. //function implementation  
  22. int nextPermutation(int target[],int end){  
  23.     int i,j;  
  24.     for (i=end-1;i>=0;i--)   
  25.         if (target[i]<target[i+1]) break;  
  26.     if (i<0) return 0; //means the permutation is over  
  27.     j=i+1;  
  28.     int k;  
  29.     for (k=end;target[k]<=target[i];k--);  
  30.     Swap(target[i],target[k]);  
  31.     Reverse(target,j,end);  
  32.     return 1;  
  33. }   
  34. void printArray (int target[],int length){  
  35.     int i;  
  36.     for (i=0;i<length;i++) printf ("%d-",target[i]);  
  37.     printf ("/n");  
  38. }  
  39. //test function  
  40. void testPermute(){  
  41.     printf ("input a number:");  
  42.     int n;  
  43.     scanf ("%d",&n);  
  44.     int i;  
  45.     int * testcase = (int*)malloc(n*sizeof(int));  
  46.     for (i=0;i<n;i++) testcase[i]=i+1;  
  47.     printArray (testcase,n);  
  48.     while (nextPermutation(testcase,n-1)){  
  49.         printArray (testcase,n);  
  50.     }  
  51. }  
  52. //main function  
  53. void main(){  
  54.     testPermute();  
  55. }  

代码完全按照算法来做,大家应该不难看懂。我就不废话了。

(3)深搜回溯算法

深度搜索,判重,回溯,这个算法应该算是一个基础了。其中八皇后问题是它的经典应用。说实话感觉它就像个万金油一样,在啥地方都能使,但是就是不一定好使就对了。不过还是要实现一下。

 其实细想一下,全排列跟八皇后问题是一样一样的。也是穷举一个数列,只是八皇后对数列的条件跟严格,也即是判重函数不同而已。没有区别的。看看代码:

Code:

  1. //n=8 Time Consumed 11.78s  
  2. #include <stdio.h>  
  3. #include <stdlib.h>  
  4. //function definition  
  5. void printArray (const int[],int);  
  6. bool isExist(const target[],int);  
  7. void permute(int[],const int,int);  
  8. //function implementation  
  9. void printArray(const int target[],int n){  
  10.     int i;  
  11.     for (i=0;i<n;i++)  
  12.         printf ("%d",target[i]);  
  13.     printf ("/n");  
  14. }  
  15. bool isExist(const int target[],int pos){  
  16.     if (pos==0) return false;  
  17.     int i;  
  18.     for (i=0;i<pos;i++)   
  19.         if (target[i]==target[pos]) return true;  
  20.     return false;  
  21. }  
  22. void permute (int target[],const int n,int i){  
  23.     if (i==n) {  
  24.         printArray(target,n);  
  25.         return;  
  26.     }  
  27.     for (target[i]=1;target[i]<=n;target[i]++)  
  28.         if (isExist(target,i) == false) permute(target,n,i+1);  
  29.     //target[i]=1;  
  30.     return;  
  31. }  
  32. //test functions  
  33. void testPermutation(){  
  34.     int n;  
  35.     printf ("Input a Number:");  
  36.     scanf ("%d",&n);  
  37.     int * testcase=(int*)malloc(n*sizeof(int));  
  38.     int i;  
  39.     for (i=0;i<n;i++) testcase[i]=1;  
  40.     permute(testcase,n,0);  
  41. }  
  42. //main function  
  43. void main(){  
  44.     testPermutation();    
  45. }  

注意,我在递归函数permute中,回溯甚至没有将当时位置上的数消掉。看被注释掉的target[i]=1。而是直接在for循环中添加初始语句target[i]=1。这就表示说只要进行深一步搜索,都先将下一位置的数初始化掉。这样就不用在回溯时对当前位置的错误值进行处理了。更加简化了。

再看看非递归版的,也就是使用堆栈的形式:

Code:

  1. // when n=8 time consumed 11.77s  
  2. #include <stdio.h>  
  3. #include <stdlib.h>  
  4. //function definition  
  5. void permute(int [],int);  
  6. void printArray(const int[],const int);  
  7. bool isExist(const int[],const int);  
  8. //function implementation  
  9. void printArray(const int target[],const int n){  
  10.     int i;  
  11.     for (i=0;i<n;i++)   
  12.         printf ("%d",target[i]);  
  13.     printf ("/n");  
  14. }  
  15. bool isExist(const int target[],int pos){  
  16.     if (pos==0) return false;  
  17.     int i;  
  18.     for (i=0;i<pos;i++)   
  19.         if (target[i]==target[pos]) return true;  
  20.     return false;  
  21. }  
  22. void permute(int stack[],int n){  
  23.     int sp=0;   //stack pointer  
  24.     while (sp>=0) {  
  25.         stack[sp]++;  
  26.         while (stack[sp]<=n && isExist(stack,sp)==true ) stack[sp]++;  
  27.         if (stack[sp]<=n) {  
  28.             //when stack is full,output the result  
  29.             //and then backtrack  
  30.             if (sp==n-1) {  
  31.                 printArray(stack,n);  
  32.                 sp--;  
  33.             }  
  34.             else {  
  35.             //push stack, which means search foward  
  36.                 sp++;  
  37.                 stack[sp]=0;  
  38.             }  
  39.         }  
  40.         else {  
  41.         //backtrack  
  42.             sp--;  
  43.         }  
  44.     }  
  45.     return;  
  46. }  
  47. //test function  
  48. void testPermutation(){  
  49.     int n;  
  50.     printf ("Input a Number:");  
  51.     scanf ("%d",&n);  
  52.     int i;  
  53.     int * testcase=(int*)malloc(n*sizeof(int));  
  54.     for (i=0;i<n;i++) testcase[i]=0;  
  55.     permute(testcase,n);  
  56. }  
  57. //main function  
  58. void main(){  
  59.     testPermutation();  
  60. }  

非递归形式算法思想是一样的,不过就是需要自己来维护一个堆栈。注意在循环中,需要首先对堆栈元素进行自加操作,不然的话,回溯之后,就不会改变堆栈值,而使程序陷入死循环。不过当然,你要使用do while来代替这种写法,那当然也是没有问题的。

我只是想说,只要是递归形式的算法,都可以用堆栈来实现非递归的形式。大家不妨来探讨一下这个一一对应的转换的具体形式。

组合

关于组合,我有一个比较帅的算法,在这里跟大家分享一下。呵呵。

这是非递归的算法,我感觉跟全排列的字典序列算法思想上比较像。

你看它是怎么实现的:

求n个数中的m个数的组合。

首先,初始化一个n个元素的数组(全部由0,1组成),将前m个初始化为1,后面的为0。这时候就可以输出第一个组合了。为1的元素的下标所对应的数。

算法开始:从前往后找,找到第一个10组合,将其反转成01,然后将其前面的所有1,全部往左边推,即保证其前面的1都在最左边。然后就可以按照这个01序列来输出一个组合结果了。

而如果找不到10组合,就表示说所有情况都输出了,为什么?你想,(以n=5,m=3为例)一开始是11100,最后就是00111,已经没有10组合了。

这种将问题转换为01序列(也就是真假序列)的想法值得我们考虑和借鉴。

最后看看实现代码:

Code:

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <string.h>  
  4. int l=0;  
  5. //function definition  
  6. void composition(const char [],int,int);   
  7. void printCompostion(const char[],const bool[],int);  
  8. //function implementation  
  9. void printCompostion(const char source[],const bool comp[],int n){  
  10.     int i;  
  11.     for (i=0;i<n;i++)   
  12.         if (comp[i]==true) printf ("%c-",source[i]);  
  13.     printf ("/n");  
  14. }  
  15. void compostion(const char* source,int n,int m){  
  16.     bool * comp = (bool*)malloc(n*sizeof(bool));  
  17.     int i;  
  18.     for (i=0;i<m;i++) comp[i]=true;  
  19.     for (i=m;i<n;i++) comp[i]=false;  
  20.     printCompostion(source,comp,n);  
  21.     l++;  
  22.     while(true){  
  23.         for (i=0;i<n-1;i++)   
  24.             if (comp[i]==true&&comp[i+1]==false) break;  
  25.         if (i==n-1) return;  //all the compostion is found out  
  26.         comp[i]=false;  
  27.         comp[i+1]=true;  
  28.         int p=0;  
  29.         while (p<i){  
  30.             while (comp[p]==true) p++;  
  31.             while (comp[i]==false) i--;  
  32.             if (p<i) {  
  33.                 comp[p]=true;  
  34.                 comp[i]=false;  
  35.             }  
  36.         }  
  37.         printCompostion(source,comp,n);  
  38.         l++;  
  39.     }  
  40. }  
  41. //test function  
  42. void testCompostion(){  
  43.     char* testcase = "abcdefghijklmno";  
  44.     int n=strlen(testcase);  
  45.     int m=7;  
  46.     compostion(testcase,n,m);  
  47. }  
  48. //main function  
  49. void main(){  
  50.     testCompostion();  
  51.     printf ("total=%d/n",l);  
  52. }  

01序列我是用bool类型来实现的。尽可能地少占用内存吧。而在实现将1往左移的步骤时,我是采用了将左边的0跟右边的1相调换的思想。

关于组合大家如果有更好的方法,不妨跟贴讨论哦。

(完)

继续阅读