題目描述
給定一個由不同的小寫字母組成的字符串,輸出這個字符串的所有全排列。
我們假設對於小寫字母有'a' < 'b' < ... < 'y' < 'z',而且給定的字符串中的字母已經按照從小到大的順序排列。
輸入
輸入只有一行,是一個由不同的小寫字母組成的字符串,已知字符串的長度在1到6之間。
輸出
輸出這個字符串的所有排列方式,每行一個排列。要求字母序比較小的排列在前面。字母序如下定義:
已知S = s1s2...sk , T = t1t2...tk,則S < T 等價於,存在p (1 <= p <= k),使得
s1 = t1, s2 = t2, ..., sp - 1 = tp - 1, sp < tp成立。
注意每組樣例輸出結束后接一個空行。
樣例輸入
xyz
樣例輸出
xyz xzy yxz yzx zxy zyx
提示
用STL中的next_permutation會非常簡潔。
思路:
由於題目提示使用next_permutation會簡潔,所以這里我們使用此方法。
1 #include<iostream> 2 #include<stdio.h> 3 #include<queue> 4 #include<string> 5 #include<string.h> 6 #include<algorithm> 7 using namespace std; 8 9 char a[10]; 10 11 int main() 12 { 13 int n; 14 while(scanf("%s",a)!=EOF) 15 { 16 n=strlen(a); 17 do 18 { 19 printf("%s\n",a); 20 }while(next_permutation(a,a+n)); 21 puts(""); 22 } 23 return 0; 24 }
C++/STL中定義的next_permutation和prev_permutation函數是非常靈活且高效的一種方法,它被廣泛的應用於為指定序列生成不同的排列。
next_permutation函數將按字母表順序生成給定序列的下一個較大的排列,直到整個序列為降序為止。
prev_permutation函數與之相反,是生成給定序列的上一個較小的排列。
所謂“下一個”和“上一個”,舉一個簡單的例子:
對序列 {a, b, c},每一個元素都比后面的小,按照字典序列,固定a之后,a比bc都小,c比b大,它的下一個序列即為{a, c, b},而{a, c, b}的上一個序列即為{a, b, c},同理可以推出所有的六個序列為:{a, b, c}、{a, c, b}、{b, a, c}、{b, c, a}、{c, a, b}、{c, b, a},其中{a, b, c}沒有上一個元素,{c, b, a}沒有下一個元素。
二者原理相同,僅遍例順序相反,這里僅以next_permutation為例介紹算法。
(1) int 類型的next_permutation
int main() { int a[3]; a[0]=1;a[1]=2;a[2]=3; do { cout<<a[0]<<" "<<a[1]<<" "<<a[2]<<endl; }while (next_permutation(a,a+3)); //參數3指的是要進行排列的長度 //如果存在a之后的排列,就返回true。如果a是最后一個排列沒有后繼,返回false,每執行一次,a就變成它的后繼 }
輸出:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
如果改成
1 while(next_permutation(a,a+2));
則輸出:
1 2 3
2 1 3
只對前兩個元素進行字典排序
顯然,如果改成
1 while(next_permutation(a,a+1));
則只輸出:1 2 3
若排列本來就是最大的了沒有后繼,則next_permutation執行后,會對排列進行字典升序排序,相當於循環
1 int list[3]={3,2,1}; 2 next_permutation(list,list+3); 3 cout<<list[0]<<" "<<list[1]<<" "<<list[2]<<endl;
輸出: 1 2 3
(2) char 類型的next_permutation
int main() { char ch[205]; cin >> ch; sort(ch, ch + strlen(ch) ); //該語句對輸入的數組進行字典升序排序。如輸入9874563102 cout<<ch;//將輸出0123456789,這樣就能輸出全排列了 char *first = ch; char *last = ch + strlen(ch); do { cout<< ch << endl; }while(next_permutation(first, last)); return 0; } //這樣就不必事先知道ch的大小了,是把整個ch字符串全都進行排序 //若采用 while(next_permutation(ch,ch+5)); 如果只輸入1562,就會產生錯誤,因為ch中第五個元素指向未知 //若要整個字符串進行排序,參數5指的是數組的長度,不含結束符
(3) string 類型的next_permutation
int main() { string line; while(cin>>line&&line!="#") { if(next_permutation(line.begin(),line.end())) //從當前輸入位置開始 cout<<line<<endl; else cout<<"Nosuccesor\n"; } }
int main() { string line; while(cin>>line&&line!="#") { sort(line.begin(),line.end());//全排列 cout<<line<<endl; while(next_permutation(line.begin(),line.end())) cout<<line<<endl; } }
next_permutation 自定義比較函數
#include<iostream> //poj 1256 Anagram #include<cstring> #include<algorithm> using namespace std; int cmp(char a,char b) //'A'<'a'<'B'<'b'<...<'Z'<'z'. { if(tolower(a)!=tolower(b)) return tolower(a)<tolower(b); else return a<b; } int main() { char ch[20]; int n; cin>>n; while(n--) { scanf("%s",ch); sort(ch,ch+strlen(ch),cmp); do { printf("%s\n",ch); }while(next_permutation(ch,ch+strlen(ch),cmp)); } return 0; }
用next_permutation和prev_permutation求排列組合很方便,但是要記得包含頭文件#include <algorithm>。
雖然最后一個排列沒有下一個排列,用next_permutation會返回false,但是使用了這個方法后,序列會變成字典序列的第一個,如cba變成abc。prev_permutation同理。
全排列生成算法
對於給定的集合A{a1,a2,...,an},其中的n個元素互不相同,如何輸出這n個元素的所有排列(全排列)。
遞歸算法
這里以A{a,b,c}為例,來說明全排列的生成方法,對於這個集合,其包含3個元素,所有的排列情況有3!=6種,對於每一種排列,其第一個元素有3種選擇a,b,c,對於第一個元素為a的排列,其第二個元素有2種選擇b,c;第一個元素為b的排列,第二個元素也有2種選擇a,c,……,依次類推,我們可以將集合的全排列與一棵多叉樹對應。如下圖所示
在此樹中,每一個從樹根到葉子節點的路徑,就對應了集合A的一個排列。通過遞歸算法,可以避免多叉樹的構建過程,直接生成集合A的全排列,代碼如下。
1 template <typename T> 2 inline void swap(T* array, unsigned int i, unsigned int j) 3 { 4 T t = array[i]; 5 array[i] = array[j]; 6 array[j] = t; 7 } 8 9 /* 10 * 遞歸輸出序列的全排列 11 */ 12 void FullArray(char* array, size_t array_size, unsigned int index) 13 { 14 if(index >= array_size) 15 { 16 for(unsigned int i = 0; i < array_size; ++i) 17 { 18 cout << array[i] << ' '; 19 } 20 21 cout << '\n'; 22 23 return; 24 } 25 26 for(unsigned int i = index; i < array_size; ++i) 27 { 28 swap(array, i, index); 29 30 FullArray1(array, array_size, index + 1); 31 32 swap(array, i, index); 33 } 34 }
字典序
全排列生成算法的一個重要思路,就是將集合A中的元素的排列,與某種順序建立一一映射的關系,按照這種順序,將集合的所有排列全部輸出。這種順序需要保證,既可以輸出全部的排列,又不能重復輸出某種排列,或者循環輸出一部分排列。字典序就是用此種思想輸出全排列的一種方式。這里以A{1,2,3,4}來說明用字典序輸出全排列的方法。
首先,對於集合A的某種排列所形成的序列,字典序是比較序列大小的一種方式。以A{1,2,3,4}為例,其所形成的排列1234<1243,比較的方法是從前到后依次比較兩個序列的對應元素,如果當前位置對應元素相同,則繼續比較下一個位置,直到第一個元素不同的位置為止,元素值大的元素在字典序中就大於元素值小的元素。上面的a1[1...4]=1234和a2[1...4]=1243,對於i=1,i=2,兩序列的對應元素相等,但是當i=2時,有a1[2]=3<a2[2]=4,所以1234<1243。
使用字典序輸出全排列的思路是,首先輸出字典序最小的排列,然后輸出字典序次小的排列,……,最后輸出字典序最大的排列。這里就涉及到一個問題,對於一個已知排列,如何求出其字典序中的下一個排列。這里給出算法。
- 對於排列a[1...n],找到所有滿足a[k]<a[k+1](0<k<n-1)的k的最大值,如果這樣的k不存在,則說明當前排列已經是a的所有排列中字典序最大者,所有排列輸出完畢。
- 在a[k+1...n]中,尋找滿足這樣條件的元素l,使得在所有a[l]>a[k]的元素中,a[l]取得最小值。也就是說a[l]>a[k],但是小於所有其他大於a[k]的元素。
- 交換a[l]與a[k].
- 對於a[k+1...n],反轉該區間內元素的順序。也就是說a[k+1]與a[n]交換,a[k+2]與a[n-1]交換,……,這樣就得到了a[1...n]在字典序中的下一個排列。
這里我們以排列a[1...8]=13876542為例,來解釋一下上述算法。首先我們發現,1(38)76542,括號位置是第一處滿足a[k]<a[k+1]的位置,此時k=2。所以我們在a[3...8]的區間內尋找比a[2]=3大的最小元素,找到a[7]=4滿足條件,交換a[2]和a[7]得到新排列14876532,對於此排列的3~8區間,反轉該區間的元素,將a[3]-a[8],a[4]-a[7],a[5]-a[6]分別交換,就得到了13876542字典序的下一個元素14235678。下面是該算法的實現代碼
字典序算法還有一個優點,就是不受重復元素的影響。例如1224,交換中間的兩個2,實際上得到的還是同一個排列,而字典序則是嚴格按照排列元素的大小關系來生成的。對於包含重復元素的輸入集合,需要先將相同的元素放在一起,以集合A{a,d,b,c,d,b}為例,如果直接對其索引123456進行全排列,將不會得到想要的結果,這里將重復的元素放到相鄰的位置,不同元素之間不一定有序,得到排列A'{a,d,d,b,b,c},然后將不同的元素,對應不同的索引值,生成索引排列122334,再執行全排列算法,即可得到最終結果。