1. 程式人生 > >關於全排列組合演算法

關於全排列組合演算法

全排列是將一組數按一定順序進行排列,如果這組數有n個,那麼全排列數為n!個。現以{1, 2, 3, 4, 5}為

例說明如何編寫全排列的遞迴演算法。



1、首先看最後兩個數4, 5。 它們的全排列為4 5和5 4, 即以4開頭的5的全排列和以5開頭的4的全排列。

由於一個數的全排列就是其本身,從而得到以上結果。

2、再看後三個數3, 4, 5。它們的全排列為3 4 5、3 5 4、 4 3 5、 4 5 3、 5 3 4、 5 4 3 六組數。

即以3開頭的和4,5的全排列的組合、以4開頭的和3,5的全排列的組合和以5開頭的和3,4的全排列的組合.

從而可以推斷,設一組數p = {r1, r2, r3, ... ,rn}, 全排列為perm(p),pn = p - {rn}。

因此perm(p) = r1perm(p1), r2perm(p2), r3perm(p3), ... , rnperm(pn)。當n = 1時perm(p} = r1。

為了更容易理解,將整組數中的所有的數分別與第一個數交換,這樣就總是在處理後n-1個數的全排列。



演算法如下:

#include <stdio.h>int n =0;  

void swap(int*a, int*b) 
{     
    
int m;     
    m 
=*a;     
    
*=*b;     
    
*= m; 
}  
void perm(int list[], int k, int m) 
{     
    
int i;     
    
if(k > m)     
    {          
        
for(i =0; i <= m; i++)             
            printf(
"%d ", list[i]);         
        printf(
"/n");         
        n
++;     
    }     
    
else     
    {         
        
for(i = k; i <= m; i++)         
        {             
            swap(
&list[k], &list[i]);             
            perm(list, k 
+1, m);             
            swap(
&list[k], &list[i]);         
        }     
    } 

int main() 
{     
    
int list[] = {12345};     
    perm(list, 
04);     
    printf(
"total:%d/n", n);     
    
return0

全排列演算法的遞迴與非遞迴實現.出於語言特性問題,執行效率較低.

< script language = " JavaScript " >
<!--
// 全排列遞迴演算法
//
code by meixx(梅雪香)
/*
遞迴的演算法採用分而治之的思想 
考慮序列 P1P2P3Pn 可以分解為 P1 + C(P2P3Pn),依此類推.
*/

function  combination(arr) {
    
var  len  =  arr.length;
    
if (len  == 2 ) {
        
var  a  =  arr[ 0 ], b  =  arr[ 1 ];
        
return  [a + b,b + a];
    }

    
else if (len  == 1 )   return  arr;
    
else {
        
var  strRtn  = "" ;
        
for ( var  i = 0 ;i < len;i ++ ) {
            strRtn 
+=  merge(arr[i],combination(arr.slice( 0 ,i).concat(arr.slice(i + 1 ,len)))).join( " , " ) + " , " ;
        }

        
return  strRtn.replace( / /,$ / , "" ).split( " , " );
    }

}

function  merge(head,arr) {
    
for ( var  i = 0 ;i < arr.length;i ++ )
        arr[i] 
=  head  +  arr[i];
    
return  arr;
}

/*
var ar = combination("54321".split(""));
for(var i=0;i<ar.length;i++)
    document.write(ar[i].join(""),"<br>");
*/

// -->
</ script >
< script language = " JavaScript " >
<!--
// 全排列非遞迴演算法
//
code by meixx(梅雪香)
/*
非遞迴全排列演算法的基本思想是:
    1.找到所有排列中最小的一個排列P.
    2.找到剛剛好比P大比其它都小的排列Q,
    3.迴圈執行第二步,直到找到一個最大的排列,演算法結束.
下面用數學的方法描述:
給定已知序列 P =  A1A2A3An ( Ai!=Aj , (1<=i<=n  , 1<=j<=n, i != j  ) )
找到P的一個最小排列Pmin = P1P2P3Pn  有  Pi > P(i-1) (1 < i <= n)
從Pmin開始,總是目前得到的最大的排列為輸入,得到下一個排列.
方法為:
1.從低位到高位(從後向前),找出“不符合趨勢”的數字。即找到一個Pi,使Pi < P(i+1)。
  若找不到這樣的pi,說明我們已經找到最後一個全排列,可以返回了。
2.在 P(i+1)P(i+2)Pn 中,找到一個Pj,便得 Pj"剛剛好大於"Pi. 
  ("剛剛好大於"的意思是:在 P(i+1)P(i+2)Pn 中所有大於Pi的元素構成的集合中最小的元素.)
3.交換 Pi , Pj 的位置.注意:此處不改變i和j的值,改變的是Pi和Pj.
4.交換後, P1P2P3Pn  並不是準確的後一個排列。因為根據第1步的查詢,我們有P(i+1) > P(i+2) > . > Pn
  即使進行了Pi和Pj的交換,這仍然是這一部分最大的一個排列。將此排列逆序倒置(變成最小的排列)即為所求的下一個排列.
5.重複步驟1-4,直到步驟1中找不到“不符合趨勢”的數字.
*/

// 引數arr:待進行全排列的陣列(沒有重複的元素)
function  Combin(arr) {
    
var  arResult  =  [];
    
var  ar  =  arr.sort();
    arResult.push(ar);
    
for (;;) {
        ar 
=  FindNext(arResult[ 0 ],ar);
        
if ( ! ar)  return  arResult;
        arResult.push(ar);
    }

}

function  FindNext(arFirst,arLast) {
    
for ( var  i = arLast.length - 1 ;i > 0 ;i -- ) {
        
if (arLast[i - 1 <  arLast[i]) // 找到了"不符合趨勢"的數字
var  ar  =  arLast.slice();
            
var  strTail  =  ar.slice(i).join( "" );
            
var  tmpStr  =  arFirst.join( "" );
            
var  strSearch  =  tmpStr.substr( tmpStr.indexOf(ar[i - 1 ])  + 1  );
            
// 確定ar[i-1]要交換的字元及該字元的位置
for ( var  j = 0 ,k = strSearch.length;j < k;j ++ ) {
                
var  ch  =  strSearch.charAt(j);
                
var  idx  =  strTail.indexOf(ch);
                
if ( idx  >= 0  )  break ;
            }

            ar[i 
+  idx]  =  ar[i - 1 ];
            ar[i
- 1 =  ch;
            
return  ar.slice( 0 ,i).concat(ar.slice(i).reverse());
        }

    }

    
return null // 找不到"不符合趨勢"的數字,說明所有的排列已經找到
}

/*
var ar = Combin("f4e3r21".split(""));
for(var i=0;i<ar.length;i++)
    document.write(ar[i].join(""),"<br>");
*/

// -->
</ script >

 儘管排列組合是生活中經常遇到的問題,可在程式設計時,不深入思考或者經驗不足都讓人無從下手。由於排列組合問題總是先取組合再排列,並且單純的排列問題相對簡單,所以本文僅對組合問題的實現進行詳細討論。以在n個數中選取m(0<m<=n)個數為例,問題可分解為:
1. 首先從n個數中選取編號最大的數,然後在剩下的n-1個數裡面選取m-1個數,直到從n-(m-1)個數中選取1個數為止。
2. 從n個數中選取編號次小的一個數,繼續執行1步,直到當前可選編號最大的數為m。
很明顯,上述方法是一個遞迴的過程,也就是說用遞迴的方法可以很乾淨利索地求得所有組合。
下面是遞迴方法的實現:
/// 求從陣列a[1..n]中任選m個元素的所有組合。
/// a[1..n]表示候選集,n為候選集大小,n>=m>0。
/// b[1..M]用來儲存當前組合中的元素(這裡儲存的是元素下標),
/// 常量M表示滿足條件的一個組合中元素的個數,M=m,這兩個引數僅用來輸出結果。
void combine( int a[], int n, int m,  int b[], const int M )
{
 for(int i=n; i>=m; i--)   // 注意這裡的迴圈範圍
 {
  b[m-1] = i - 1;
  if (m > 1)
   combine(a,i-1,m-1,b,M);
  else                     // m == 1, 輸出一個組合
  {  
   for(int j=M-1; j>=0; j--)
    cout << a[b[j]] << " ";
   cout << endl;
  }
 }
}

因為遞迴程式均可以通過引入棧,用回溯轉化為相應的非遞迴程式,所以組合問題又可以用回溯的方法來解決。為了便於理解,我們可以把組合問題化歸為圖的路徑遍歷問題,在n個數中選取m個數的所有組合,相當於在一個這樣的圖中(下面以從1,2,3,4中任選3個數為例說明)求從[1,1]位置出發到達[m,x](m<=x<=n)位置的所有路徑:
1  2  3  4
    2  3  4
        3  4
上圖是擷取n×n右上對角矩陣的前m行構成,如果把矩矩中的每個元素看作圖中的一個節點,我們要求的所有組合就相當於從第一行的第一列元素[1,1]出發,到第三行的任意一列元素作為結束的所有路徑,規定只有相鄰行之間的節點,並且下一行的節點必須處於上一行節點右面才有路徑相連,其他情況都無路徑相通。顯然,任一路徑經過的數字序列就對應一個符合要求的組合。
下面是非遞迴的回溯方法的實現:
/// 求從陣列a[1..n]中任選m個元素的所有組合。
/// a[1..n]表示候選集,m表示一個組合的元素個數。
/// 返回所有組合的總數。
int combine(int a[], int n, int m)
{  
 m = m > n ? n : m;

 int* order = new int[m+1];   
 for(int i=0; i<=m; i++)
  order[i] = i-1;            // 注意這裡order[0]=-1用來作為迴圈判斷標識
 
 int count = 0;                               
 int k = m;
 bool flag = true;           // 標誌找到一個有效組合
 while(order[0] == -1)
 {
  if(flag)                   // 輸出符合要求的組合
  {  
   for(i=1; i<=m; i++)                   
    cout << a[order[i]] << " ";
   cout << endl;
   count++;
   flag = false;
  }

  order[k]++;                // 在當前位置選擇新的數字
  if(order[k] == n)          // 當前位置已無數字可選,回溯
  {
   order[k--] = 0;
   continue;
  }    
 
  if(k < m)                  // 更新當前位置的下一位置的數字         
  {
   order[++k] = order[k-1];
   continue;
  }
 
  if(k == m)
   flag = true;
 }

 delete[] order;
 return count;
}

下面是測試以上函式的程式:
int main()
{
 const int N = 4;
 const int M = 3;
 int a[N];
 for(int i=0;i<N;i++)
  a[i] = i+1;

 // 回溯方法
 cout << combine(a,N,3) << endl;

 // 遞迴方法
 int b[M];
 combine(a,N,M,b,M);

 return 0;
}

由上述分析可知,解決組合問題的通用演算法不外乎遞迴和回溯兩種。在針對具體問題的時候,因為遞迴程式在遞迴層數上的限制,對於大型組合問題而言,遞迴不是一個好的選擇,這種情況下只能採取回溯的方法來解決。

    n個數的全排列問題相對簡單,可以通過交換位置按序列舉來實現。STL提供了求某個序列下一個排列的演算法next_permutation,其演算法原理如下:
1. 從當前序列最尾端開始往前尋找兩個相鄰元素,令前面一個元素為*i,後一個元素為*ii,且滿足*i<*ii;
2. 再次從當前序列末端開始向前掃描,找出第一個大於*i的元素,令為*j(j可能等於ii),將i,j元素對調;
3. 將ii之後(含ii)的所有元素顛倒次序,這樣所得的排列即為當前序列的下一個排列。
其實現程式碼如下:
template <class BidirectionalIterator>
bool next_permutation(BidirectionalIterator first, BidirectionalIterator last)
{
  if (first == last) return false;   // 空範圍
  BidirectionalIterator i = first;
  ++i;
  if (i == last) return false;       // 只有一個元素
  i = last;                          // i 指向尾端
  --i;

 for(;;)
 {
  BidirectionalIterator ii = i;
  --i;
  // 以上,鎖定一組(兩個)相鄰元素
  if (*i < *ii)                     // 如果前一個元素小於後一個元素
  {
   BidirectionalIterator j = last;  // 令 j指向尾端
   while (!(*i < *--j));            // 由尾端往前找,直到遇上比 *i 大的元素
   iter_swap(i, j);                 // 交換 i, j
   reverse(ii, last);               // 將 ii 之後的元素全部逆向重排
   return true;
  }
  if (i == first)                   // 進行至最前面了
  {
   reverse(first, last);            // 全部逆向重排
   return false;
  }
 }
}

下面程式演示了利用next_permutation來求取某個序列全排列的方法:
int main()
{
 int ia[] = {1,2,3,4};
 vector<int> iv(ia,ia+sizeof(ia)/sizeof(int));

 copy(iv.begin(),iv.end(),ostream_iterator<int>(cout," "));
 cout << endl;
 while(next_permutation(iv.begin(),iv.end()))
 {
  copy(iv.begin(),iv.end(),ostream_iterator<int>(cout," "));
  cout << endl;
 }

 return 0;
}
注意:上面程式中初始序列是按數值的從小到大的順序排列的,如果初始序列無序的話,上面程式只能求出從當前序列開始的後續部分排列,也就是說next_permutation求出的排列是按排列從小到大的順序進行的。

組合演算法
本程式的思路是開一個數組,其下標表示1到m個數,陣列元素的值為1表示其下標
代表的數被選中,為0則沒選中。
首先初始化,將陣列前n個元素置1,表示第一個組合為前n個數。
然後從左到右掃描陣列元素值的“10”組合,找到第一個“10”組合後將其變為
“01”組合,同時將其左邊的所有“1”全部移動到陣列的最左端。
當第一個“1”移動到陣列的m-n的位置,即n個“1”全部移動到最右端時,就得
到了最後一個組合。