天天看點

排列與組合的算法實作

由于老師提出了全排列問題。我心血來潮,将組合也弄了一下。 現在寫篇日志記錄一下。

全排列

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

(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相調換的思想。

關于組合大家如果有更好的方法,不妨跟貼讨論哦。

(完)

繼續閱讀