排列組合算法


排列組合算法

1。最近一直在考慮從m個數里面取n個數的算法。最容易理解的就是遞歸,但是其效率,實在不能使用。一直找尋中,今日得果

2。算法來源與互聯網

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

全排列算法  
   
  從1到N,輸出全排列,共N!條。  
  分析:用N進制的方法吧。設一個N個單元的數組,對第一個單元做加一操作,滿N進  
  一。每加一次一就判斷一下各位數組單元有無重復,有則再轉回去做加一操作,沒  
  有則說明得到了一個排列方案。

 //////////////////////////////////////////////////////

遞歸算法


    全排列是將一組數按一定順序進行排列,如果這組數有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;    
    *a = *b;    
    *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[] = {1, 2, 3, 4, 5};    
    perm(list, 0, 4);    
    printf("total:%d/n", n);    
    return 0;


誰有更高效的遞歸和非遞歸算法,請回貼。

///////////////////////////////////////

全排序

設R={r1,r2,...,rn}是要進行排列的n個元素,Ri = R-{ri}. 集合 X 中元素的全排列記為Perm(X)。(ri)Perm(X)表示在全排列Perm(X)的每一個排列上加前綴ri得到的排列。R的全排列可歸納定義如下:

當 n = 1 時, Perm(R) = (r),其中r 是集合R中唯一的元素;

當 n >1 時, Perm(R)有 (r1)Perm(R1),(r2)Perm(R2),.......,(rn)Perm(Rn)構成

依此遞歸定義,可設計產生Perm(R)的遞歸算法如下:

template <class Type>

void Perm(Type list[], int k, int m){

    if ( k == m ){

        for ( int i = 0; i <= m; i++)

                    cout << list[i];

        cout << endl;

    }

    else{

        for ( int i = k; i <= m; i ++){

           Swap( list[k],list[i] );

           Perm( list,k + 1, m ) ;

         Swap( list[k], list[i] );

         }

    }

}

template < class Type >

inline void Swap ( Type &a ,Type & b)

{

         Type temp = a; a = b; b = temp;

}

////////////////////////////////////////

  排列組合問題的通用算法

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

///////////////////////////////////////////////////////

排列組合與回溯算法

KuiBing

感謝Bamboo、LeeMaRS的幫助

[關鍵字] 遞歸 DFS

[前言] 這篇論文主要針對排列組合對回溯算法展開討論,在每一個討論之后,還有相關的推薦題。在開始之前,我們先應該看一下回溯算法的概念,所謂回溯:就是搜索一棵狀態樹的過程,這個過程類似於圖的深度優先搜索(DFS),在搜索的每一步(這里的每一步對應搜索樹的第i層)中產生一個正確的解,然后在以后的每一步搜索過程中,都檢查其前一步的記錄,並且它將有條件的選擇以后的每一個搜索狀態(即第i+1層的狀態節點)。

需掌握的基本算法:

排列:就是從n個元素中同時取r個元素的排列,記做P(n,r)。(當r=n時,我們稱P(n,n)=n!為全排列)例如我們有集合OR = {1,2,3,4},那么n = |OR| = 4,切規定r=3,那么P(4,3)就是:

{1,2,3}; {1,2,4}; {1,3,2}; {1,3,4};{1,4,2};{1,4,3};{2,1,3};{2,1,4}; {2,3,1}; {2,3,4}; {2,4,1}; {2,4,3}; {3,1,2}; {3,1,4}; {3,2,1}; {3,2,4}; {3,4,1}; {3,4,2}; {4,1,2}; {4,1,3}; {4,2,1}; {4,2,3}; {4,3,1}; {4,3,2}

算法如下:

int  n, r;
char used[MaxN];
int  p[MaxN];
 
void permute(int pos)
{ int i;
/*如果已是第r個元素了,則可打印r個元素的排列 */
    if (pos==r) {
        for (i=0; i<r; i++)
            cout << (p[i]+1);
        cout << endl;
        return;
    }
    for (i=0; i<n; i++)
        if (!used[i]) { /*如果第i個元素未用過*/
/*使用第i個元素,作上已用標記,目的是使以后該元素不可用*/
            used[i]++;
/*保存當前搜索到的第i個元素*/
            p[pos] = i;
/*遞歸搜索*/
           permute(pos+1);
 
/*恢復遞歸前的值,目的是使以后改元素可用*/
 used[i]--;
        }
}

相關問題
UVA 524 Prime Ring Problem

 

可重排列:就是從任意n個元素中,取r個可重復的元素的排列。例如,對於集合OR={1,1,2,2}, n = |OR| = 4, r = 2,那么排列如下:

{1,1}; {1,2}; {1,2}; {1,1}; {1,2}; {1,2}; {2,1}; {2,1}; {2,2}; {2,1}; {2,1}; {2,2}

則可重排列是:

{1,1}; {1,2}; {2,1}; {2,2}.

算法如下:

#define FREE -1
int n, r;
/*使元素有序*/
int E[MaxN] = {0,0,1,1,1};
int P[MaxN];
char used[MaxN];
 
void permute(int pos)
{
int i;
/*如果已選了r個元素了,則打印它們*/
    if (pos==r)  {
        for (i=0; i<r; i++)
            cout << P[i];
        cout << endl;
        return;
    }
/*標記下我們排列中的以前的元素表明是不存在的*/
    P[pos] = FREE;
    for (i=0; i<n; i++)
/*如果第I個元素沒有用過,並且與先前的不同*/
        if (!used[i] && E[i]!=P[pos]) {
/*使用這個元素*/
            used[i]++;
/*選擇現在元素的位置*/
            P[pos] = E[i];
/*遞歸搜索*/
            permute(pos+1);
/*恢復遞歸前的值*/
            used[i]--;
        }
}

相關習題
UVA 10098 Generating Fast, Sorted Permutations

 

組合:從n個不同元素中取r個不重復的元素組成一個子集,而不考慮其元素的順序,稱為從n個中取r個的無重組合,例如OR = {1,2,3,4}, n = 4, r = 3則無重組合為:

{1,2,3}; {1,2,4}; {1,3,4}; {2,3,4}.

算法如下:

int n, r;
int C[5];
char used[5];
 
void combine(int pos, int h)
{
int i;
/*如果已選了r個元素了,則打印它們*/
    if (pos==r) {
        for (i=0; i<r; i++)
            cout<< C[i];
        cout<< endl;
        return;
    }
    for (i=h; i<=n-r+pos; i++) /*對於所有未用的元素*/
        if (!used[i]) {
/*把它放置在組合中*/
            C[pos] = i;
/*使用該元素*/
 used[i]++;
/*搜索第i+1個元素*/
     combine(pos+1,i+1);
/*恢復遞歸前的值*/
 used[i]--;
        }
}

相關問題:
Ural 1034 Queens in peaceful position

 

可重組合:類似於可重排列。

[例] 給出空間中給定n(n<10)個點,畫一條簡單路徑,包括所有的點,使得路徑最短。

解:這是一個旅行售貨員問題TSP。這是一個NP問題,其實就是一個排列選取問題。

算法如下:

int  n, r;
char used[MaxN];
int  p[MaxN];
double min;
 
void permute(int pos, double dist)
{
int i;
    if (pos==n) {
        if (dist < min) min = dist;
        return;
    }
    for (i=0; i<n; i++)
        if (!used[i]) {
            used[i]++;
            p[pos] = i;
           if (dist + cost(point[p[pos-1]], point[p[pos]]) < min)
                permute(pos+1, dist + cost(point[p[pos-1]], point[p[pos]]));
 used[i]--;
        }
}

[例]對於0和1的所有排列,從中同時選取r個元素使得0和1的數量不同。

解 這道題很簡單,其實就是從0到2^r的二元表示。

算法如下:

void dfs(int pos)
{
   if (pos == r)
   {
       for (i=0; i<r; i++) cout<<p[i];
       cout<<endl;
       return;
   }
   p[pos] = 0;
   dfs(pos+1);
   p[pos] = 1;
   dfs(pos+1);}

相關問題:

Ural

1005 Stone pile
1060 Flip Game
1152 The False Mirrors

 

[例]找最大團問題。

一個圖的團,就是包括了圖的所有點的子圖,並且是連通的。也就是說,一個子圖包含了n個頂點和n*(n-1)/2條邊,找最大團問題是一個NP問題。算法如下:

#define MaxN 50
 
int  n, max;
int  path[MaxN][MaxN];
int  inClique[MaxN];
 
void dfs(int inGraph[])
{
int i, j;
int Graph[MaxN];
 
if ( inClique[0]+inGraph[0]<=max ) return;
if ( inClique[0]>max ) max=inClique[0];
 
/*對於圖中的所有點*/
    for (i=1; i<=inGraph[0]; i++)
    {
/*把節點放置到團中*/
        ++inClique[0];
 inClique[inClique[0]]=inGraph[i];
/*生成一個新的子圖*/
 Graph[0]=0;
 for (j=i+1; j<=inGraph[0]; j++)
     if (path[inGraph[i]][inGraph[j]] )
          Graph[++Graph[0]]=inGraph[j];
     dfs(Graph);
/*從團中刪除節點*/
        --inClique[0];}
}
int main()
{
int inGraph[MaxN];
int i, j;
  cin >>n;
  while (n > 0)
  {
        for (i=0; i<n; i++)
 for (j=0; j<n; j++)
     cin >>path[i][j];
        max = 1;
/*初始化*/
        inClique[0]= 0;
        inGraph[0] = n;
 for (i=0; i<n; i++) inGraph[i+1]=i;
        dfs(inGraph);
        cout<<max<<endl;
        cin >>n;
  }
  return 0;}

 

 參考論文 <A fast algorithm for the maximum clique problem>

相關問題:

acm.zju.edu.cn: 1492 maximum clique

 

相關網站

http://acm.uva.es/p

http://acm.timus.ru/

 

Contact me:

MSN: Bing0672@Hotmail.com

/////////////////////////

  求集合子集,和全排列的遞歸算法實現(c++,Dev C++調試通過)


求集合全排列算法實現:

求集合所有子集的算法實現:

1.求集合全排列算法實現:

/*
  Name:
  Copyright:
  Author: XuLei
  Date: 01-11-05 09:40
  Description:求一個字符串集合(List)的全排列,一共有n!種(假設字符數為n)
  Algorithms:令E= {e1 , ..., en }表示n 個元素的集合,我們的目標是生成該集合的所有排列方式。令Ei
             為E中移去元素i 以后所獲得的集合,perm (X) 表示集合X 中元素的排列方式,ei.p e r m
             (X)表示在perm (X) 中的每個排列方式的前面均加上ei 以后所得到的排列方式。例如,如果
             E={a, b, c},那么E1={b, c},perm (E1 )=( b c, c b),e1 .perm(E1) = (a b c, a c b)。
             對於遞歸的基本部分,采用n = 1。當只有一個元素時,只可能產生一種排列方式,所以
             perm (E) = (e),其中e 是E 中的唯一元素。當n > 1時,perm (E) = e1 .perm(E1) +e2 .p e r m
             (E2) +e3.perm(E3) + ... +en .perm (En)。這種遞歸定義形式是采用n 個perm(X) 來定義perm(E),
             其中每個X 包含n-1個元素。至此,一個完整的遞歸定義所需要的基本部分和遞歸部分都已完成。
*/
#include <iostream>
using namespace std;
//const int ListLength=10;
const int ListLength=3;     //字符串數組的長度
void Swap(char &c, char &s) //交換字符c和s
{
     char temp=c;
     c=s;
     s=temp;
}
void Perm(char *List, int m, int k)
{
     static int count=0;
     if(m==k)
     {
             cout<<++count<<":";
             for(int i=0; i<=ListLength-1; i++)
             {
                     cout<<List[i];
             }           
             cout<<endl;
     }
     else
     {
         for(int i=m; i<=k; i++)
         {
                  Swap(List[m],List[i]);
                  Perm(List, m+1, k);
                  Swap(List[m],List[i]);
                
         }       
     }
       
}
int main()
{
    //char List[ListLength]={'a','b','c','d','e','f','g','h','i','j'};
    char List[ListLength]={'a','b','c'};
    Perm(List, 0, ListLength-1);
    system("pause");
    return 0;

}

2. 求集合所有子集的算法實現:

/*
  Name:
  Copyright:
  Author: XuLei
  Date: 01-11-05 11:34
  Description: 求一個集合(List)的所有子集,並輸出
  Algorithms: 由SubSet函數來求所有的子集,SubSet(char *List, int m, char *Buffer, int flag)
              基本思想為首先取出List[m],然后依次把List[m+1...ListLength-1]加到List[m]后面,
              每加一個,存儲在集合Buffer[]中,並輸出。由flag標識數組Buffer的長度。
              以集合{a,b,c}為例,首先取出a存入Buffer[0],輸出。
                                 然后調用SubSet(char *List, 1, char *Buffer, 1)把Buffer[1]=b
                                    輸出ab。
                                 再調用SubSet(char *List, 2, char *Buffer, 2) 把Buffer[2]=c
                                    輸出abc。
                                 再進入SubSet(char *List, 1, char *Buffer, 1) 把Buffer[1]=c
                                    輸出ac。
                                 退回最外層的循環。
                                 取出b存入Buffer[0],輸出。
                                 然后調用SubSet(char *List, 1, char *Buffer, 1)把Buffer[1]=c
                                    輸出bc。
                                 取出c存入Buffer[0],輸出。              
*/
#include <iostream>
using namespace std;
const int ListLength=10;
//const int ListLength=3;

//輸出Buffer集合
void Output(char *Buffer, int flag)
{
     static int count=1;
     if(count==1)
     {
              cout<<count++<<": { }"<<endl;       
     }
     cout<<count++<<": {";
     for(int i=0; i<=flag; i++)
     {
              cout<<Buffer[i];           
     }
     cout<<"}"<<endl;
}
//找到元素c在集合List中的位置
int Index(char *List, char c)
{
     for(int i=0; i<=ListLength-1; i++)
     {
              if(c==List[i])
              {
                    return i;           
                    break;
              }                
     }
     return -1;   
}

void SubSet(char *List, int m, char *Buffer, int flag)
{   
     if(m <= ListLength-1)
     {
          /*if(m==0)
          {
                  Buffer[0]=List[0];
          }*/
          //Buffer[flag]=List[m];
          /*if(flag==0)
          {
                Buffer[flag]=List[m];
          }*/
        
          for(int i=(flag==0) ? 0 : Index(List,Buffer[flag-1])+1; i<=ListLength-1; i++)
          //當flag==0時,Buffer中沒有任何元素,此時i=[0...ListLength-1]
          //當flag>0時,找到Buffer中的最后一個元素在集合List中的位置i,把[i....ListLength-1]
          //處的元素,加到Buffer元素的最后面
          {
                Buffer[flag]=List[i];              
                Output(Buffer,flag);
                SubSet(List, m+1, Buffer,flag+1);
          }        
     }
     return;
}

int main()
{
    char List[ListLength]={'a','b','c','d','e','f','g','h','i','j'}; 
    //char List[ListLength]={'a','b','c'};
    char Buffer[ListLength]={' ',' ',' ',' ',' ',' ',' ',' ',' ',' '};
    //char Buffer[ListLength]={' ',' ',' '};
    //int flag=0;
    //TEST
    //cout<<Index(List,'c');  OK
    SubSet(List,0,Buffer,0);
    system("pause");
    return 0; 
}

///////////////////////////////////////////////////////////////////////

注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



排列組合算法 排列組合算法 排列組合算法 排列組合算法 排列組合算法 排列組合算法 排列組合算法 排列組合算法 排列組合算法 排列組合公式及排列組合算法
 
粤ICP备14056181号  © 2014-2021 ITdaan.com