1. 程式人生 > >排列組合問題的通用演算法

排列組合問題的通用演算法

    儘管排列組合是生活中經常遇到的問題,可在程式設計時,不深入思考或者經驗不足都讓人無從下手。由於排列組合問題總是先取組合再排列,並且單純的排列問題相對簡單,所以本文僅對組合問題的實現進行詳細討論。以在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求出的排列是按排列從小到大的順序進行的。

 [email protected]戴維 2006.5  於北京 

相關推薦

排列組合問題的通用演算法

    儘管排列組合是生活中經常遇到的問題,可在程式設計時,不深入思考或者經驗不足都讓人無從下手。由於排列組合問題總是先取組合再排列,並且單純的排列問題相對簡單,所以本文僅對組合問題的實現進行詳細討論。以在n個數中選取m(0<m<=n)個數為例,問題可分解為:1.

W3Cschool高階指令碼演算法(5.排列組合去重演算法挑戰)

排列組合去重演算法挑戰 問題: 把一個字串中的字元重新排列生成新的字串,返回新生成的字串裡沒有連續重複字元的字串個數.連續重複只以單個字元為準 例如, aab 應該返回 2 因為它總共有6中排列 (aab, aab, aba, aba, baa, baa), 但是隻有兩

關於全排列組合演算法

全排列是將一組數按一定順序進行排列,如果這組數有n個,那麼全排列數為n!個。現以{1, 2, 3, 4, 5}為例說明如何編寫全排列的遞迴演算法。1、首先看最後兩個數4, 5。 它們的全排列為4 5和5 4, 即以4開頭的5的全排列和以5開頭的4的全排列。由於一個數的全排列就

c#排列組合演算法

Combinatorics.cs程式碼清單   using System; using System.Collections; using System.Data;      &nbs

C++:排列組合演算法

轉載請註明出處1 介紹排列(Permutation)和組合(Combination)是兩個基礎的數學概念。計算排列與組合可以解決一些實際的工程問題,掌握排列組合計算的方法是十分重要的。目前,網上已經有一些計算排列組合的演算法,比如[1]。這裡我也給出一個組合計算方法。該計算方

排列組合遞迴和非遞迴演算法總結篇

#include <iostream> #include <string> #include <math.h> #include <vector> #include <algorithm> using nam

33選6演算法:M個數N個為一組,無重複的排列組合

private void button1_Click(object sender, EventArgs e) { int nCnt = 0; List<int> nNumList = new

java排列組合演算法(M選N)

從M長度的容器中每次選取N個元素 舉例,從(a,b,c,d,e)中取兩個元素,則為(ab) (ac) (ad)(ae)(bc)(bd)(be)(cd)(ce)(de) import java.util.ArrayList; import java.uti

演算法題(1)——輸出指定字串所有排列組合

題目介紹:輸入給定字串,輸出所有排列可能例如:給定字串為 1234. 輸出所有排列可能:1234 1324 1423 1432。public class Class01 { public static void main(String[] args) {

C#實現排列組合演算法

數學中排列組合,排列P(N,R) 其實排列實現了,組合也就實現了組合C(N,R)就是P(N,R)/P(R,R) ,比較簡單的是遞迴演算法,但考慮到遞迴的效能,下面採用了2種非遞迴的方法,程式碼如下 using System; using System.Collections

數學和演算法之---排列組合

本文大部分內容摘自網路只是本人稍加整理分享,供大家學習交流。 排列的概念 從n個不同的元素中,任取m(m<=n)個元素按照一定的順尋排成一列,叫做從n個不同元素中取出m個元素的一個排列。從n個不同元素中取出m個元素的排列的個數,叫做從n個不同的元素中取出m

排列組合演算法總結(基於C++實現)

1.排列 全排列n! 1.1 遞迴法 設一組數p = {r1, r2, r3, … ,rn}, 全排列為perm(p),pn = p – {rn}。則perm(p) = r1perm(p1), r2perm(p2), r3perm(p3), … , r

組合數學——排列數生成演算法詳解(zz)

 組合數學中的全排列深成演算法歷來是組合數學考試的重要考察點,因此在這裡我簡單的介紹一下6種全排列生成演算法的詳細過程,並藉此比較它們之間的優劣之處。 不論是哪種全排列生成演算法,都遵循著“原排列”→“原中介數”→“新中介數”→“新排列”的過程。其中中介數依據演算法的不同會的

java 高效率的排列組合演算法(java實現)

package BeanUtil; import java.util.ArrayList; import java.util.List; import com.work.core.exception.OurException; /** * 統計任三出現的最

排列組合和回溯演算法-面試題

public void solveSudoku(char[][] board) { ArrayList<ArrayList<Integer>> emptyLocations= new ArrayList<ArrayList<Int

演算法-把n個數的每一種排列情況都列出來(排列組合)-全排列-字典序演算法(一看就懂)

首先需要介紹字典序演算法 比如 236541想找到下一個比它大的數 他有3個步驟 1.從最右邊開始找到第一組 左小於右的數 41 54 65 36 23 這樣找,很顯然,我們找到36就找到了,後面的就不用找了。 2.找到之後立刻交換嗎?不是的。定位了這個3以後,再從右邊開始

c語言實現排列組合演算法問題

排列組合是演算法常用的基本工具,如何在c語言中實現排列組合呢?思路如下: 首先看遞迴實現,由於遞迴將問題逐級分解,因此相對比較容易理解,但是需要消耗大量的棧空間,如果執行緒棧空間不夠,那麼就執行不下去了,而且函式呼叫開銷也比較大。 (1) 全排列: 全排列表示把集合

c# C(m,n) 排列組合演算法

n從1到m的所有組合: static void Main() {     string[] arr = new string[] { "a", "b", "c", "d", "e" };     L

排列組合演算法 java

排列組合問題,最簡潔有效的方法則是利用遞迴 一、無重複資料選K個組合: /* * 無重複資料組合--dfs解法   * data為資料 re為結果, curpo為當前位置,curlen為當前結果的長度 ,K為目標長度  *例如 data=“123456”  選3個字元組合,