生成1~n的排列
我們嘗試用遞歸的思想解決:先輸出所有以1開頭的排列(這一步是遞歸調用),然后 輸出以2開頭的排列(又是遞歸調用),接着是以3開頭的排列……最后才是以n開頭的排 列。
以1開頭的排列的特點是:第一位是1,后面是2~9的排列。根據字典序的定義,這些2 ~9的排列也必須按照字典序排列。換句話說,需要“按照字典序輸出2~9的排列”,不過需 注意的是,在輸出時,每個排列的最前面要加上“1”。這樣一來,所設計的遞歸函數需要以 下參數:
已經確定的“前綴”序列,以便輸出。 需要進行全排列的元素集合,以便依次選做第一個元素。
這樣可得到一個偽代碼:
void print_permutation(序列A, 集合S)
{
if(S為空) 輸出序列A;
else 按照從小到大的順序依次考慮S的每個元素v
{
print_permutation(在A的末尾填加v后得到的新序列, S-{v});
}
}
暫時不用考慮序列A和集合S如何表示,首先理解一下上面的偽代碼。遞歸邊界是S為空 的情形,這很好理解:現在序列A就是一個完整的排列,直接輸出即可。接下來按照從小到大的順序考慮S中的每個元素,每次遞歸調用以A開頭。
下面考慮程序實現。不難想到用數組表示序列A,而集合S根本不用保存,因為它可以 由序列A完全確定——A中沒有出現的元素都可以選。C語言中的函數在接受數組參數時無法 得知數組的元素個數,所以需要傳一個已經填好的位置個數,或者當前需要確定的元素位置 cur,代碼如下:
#include<iostream>
using namespace std; const int maxn=10000; int a[maxn]; void print_permutation(int n,int a[],int cur) { if(cur==n) //遞歸邊界
{ for(int i=0;i<n;i++) cout<<a[i]; cout<<endl; } else { for(int i=1;i<=n;i++) //嘗試a[cur]中從小到大填1-n各個整數
{ int ok=1; //標志元素i是否已經用過
for(int j=0;j<cur;j++) if(a[j]==i) ok=0; //i已經用過,ok標記為0
if(ok) { a[cur]=i; //i未用過,把它添加到序列末尾后繼續遞歸調用
print_permutation(n,a,cur+1); } } } } int main() { int n; cin>>n; print_permutation(n,a,0); return 0; }
循環變量i是當前考察的A[cur]。為了檢查元素i是否已經用過,上面的程序用到了一個 標志變量ok,初始值為1(真),如果發現有某個A[j]==i時,則改為0(假)。如果最終ok仍 為1,則說明i沒有在序列中出現過,把它添加到序列末尾(A[cur]=i)后遞歸調用。
聲明一個足夠大的數組A,然后調用print_permutation(n, A, 0),即可按字典序輸出1~n的 所有排列。
給定數組的全排列
如果把問題改成:輸入數組P,並按字典序輸出數組A各元素的所有全排列,則需要對 上述程序進行修改——把P加到print_permutation的參數列表中,然后把代碼中的if(A[j] == i) 和A[cur] = i分別改成if(A[j] == P[i])和A[cur] = P[i]。這樣,只要把P的所有元素按從小到大的
順序排序,然后調用print_permutation(n, A, 0,p)即可。(注意:此方法數組內元素不可重復)
#include<iostream> #include<algorithm> using namespace std; const int maxn=10000; int a[maxn]; void print_permutation(int n,int a[],int cur,int p[]) { if(cur==n) //遞歸邊界 { for(int i=0;i<n;i++) cout<<a[i]; cout<<endl; } else { for(int i=0;i<n;i++) //嘗試a[cur]中從小到大填p[i]中的各個整數 { int ok=1; //標志元素i是否已經用過 for(int j=0;j<cur;j++) if(a[j]==p[i]) ok=0; //p[i]已經用過,ok標記為0 if(ok) { a[cur]=p[i]; //p[i]未用過,把它添加到序列末尾后繼續遞歸調用 print_permutation(n,a,cur+1,p); } } } } int main() { int p[20]; int n; cin>>n; for(int i=0;i<n;i++) cin>>p[i]; sort(p,p+n); print_permutation(n,a,0,p); return 0; }
生成可重集的排列
方法一:
一個解決方法是統計A[0]~A[cur-1]中P[i]的出現次數c1,以及P數組中P[i]的出現次數 c2。只要c1<c2,就能遞歸調用。結果又如何呢?輸入1 1 1,輸出了27個1 1 1。遺漏沒有了,但是出現了重復:先試着把 第1個1作為開頭,遞歸調用結束后再嘗試用第2個1作為開頭,遞歸調用結束后再嘗試用第3 個1作為開頭,再一次遞歸調用。可實際上這3個1是相同的,應只遞歸1次,而不是3次。
換句話說,我們枚舉的下標i應不重復、不遺漏地取遍所有P[i]值。由於P數組已經排過 序,所以只需檢查P的第一個元素和所有“與前一個元素不相同”的元素,即只需在“for(i = 0; i < n; i++)”和其后的花括號之前加上“if(!i || P[i] != P[i-1])”即可。
#include<iostream> #include<algorithm> using namespace std; const int maxn=10000; int a[maxn]; void print_permutation(int n,int a[],int cur,int p[]) { if(cur==n) //遞歸邊界 { for(int i=0;i<n;i++) cout<<a[i]; cout<<endl; } else { for(int i=0;i<n;i++) //嘗試a[cur]中從小到大填p[i]中的各個整數 { if(p[i]!=p[i-1]) { int c1=0,c2=0; for(int j=0;j<cur;j++) if(a[j]==p[i]) c1++; for(int j=0;j<n;j++) if(p[j]==p[i]) c2++; if(c1<c2) { a[cur]=p[i]; print_permutation(n,a,cur+1,p); } } } } } int main() { int p[20]; int n; cin>>n; for(int i=0;i<n;i++) cin>>p[i]; sort(p,p+n); print_permutation(n,a,0,p); return 0; }
方法二:
枚舉所有排列的另一個方法是從字典序最小排列開始,不停調用“求下一個排列”的過 程。如何求下一個排列呢?C++的STL中提供了一個庫函數next_permutation。看看下面的代 碼片段,就會明白如何使用它了。
#include<cstdio> #include<algorithm> using namespace std; int main( ) { int n, p[10]; scanf("%d", &n); for(int i = 0; i < n; i++) scanf("%d", &p[i]); sort(p, p+n); //排序,得到p的最小排列 do { for(int i = 0; i < n; i++) printf("%d ", p[i]); printf("\n"); } while(next_permutation(p, p+n)); return 0; }
需要注意的是,上述代碼同樣適用於可重集。