例:輸出從1,2......m,中任取k個數的所有組合。m=5,k=3時有543,542,541,532,531,521,432,431,421,321有C(m,k)個。
法一:枚舉方法
1 #include <iostream> 2 #include <cstdio> 3 using namespace std; 4 int main() 5 { 6 int n; 7 cin >> n; 8 for (int x = 1; x <= n - 2; ++x) 9 for (int y = x + 1; y <= n - 1; ++y) 10 for (int z = y + 1; z <= n; ++z) 11 printf("%d %d %d\n", x, y, z); 12 system("pause"); 13 return 0; 14 }
n = 5時將輸出123,124,125,134,135,145,234,235,245,345。
法二:遞歸實現
設函數comb(int m,int k)是找出1,2,.....m中任取k個數的所有組合,當組合的第一個數字選定后,其后的數字的就是從余下的m-1個數選k-1個數的組合,問題轉化為求comb(m-1, k-1).需要一個全局數組存放結果,將確定的k個數字組合的第一個數存儲在a[k]中,求出一個組合后輸出a數組中的一個組合。每個組合的第一個數字可以是m,m-1,.....k.函數確定組合的第一個數字后,如果還為確定組合的其余元素,則繼續遞歸,否則輸出。
1 #include <iostream> 2 #include <cstdio> 3 using namespace std; 4 const int MAX = 10; 5 int a[10]; 6 void comb(int m, int k) 7 { 8 int i, j; 9 for (i = m; i >= k; --i) 10 { 11 a[k] = i; //每個組合的第一個數可以是m,m-1.....k 12 if (k > 1) 13 comb(i - 1, k - 1); //k>1繼續遞歸 14 else 15 { 16 for (j = a[0]; j > 0; --j) 17 printf("%d", a[j]); 18 printf("\n"); 19 } 20 } 21 } 22 int main() 23 { 24 int m, k; 25 cin >> m >> k; 26 a[0] = k; //a[0]用來表示k 27 comb(m, k); 28 return 0; 29 }
輸入m=5,k=3,輸出543,542,541,532,531,521,432,431,421,321。
全排列的生成:
最簡單的方法就是使用STL<algorithm>中的next_permutation,其返回bool值,不過經常用就啥都忘了。
1 #include <iostream> 2 #include <cstdio> 3 #include <algorithm> //for sort and next_permutation 4 using namespace std; 5 int main() 6 { 7 int n; 8 scanf("%d", &n); 9 int p[n]; 10 for (int i = 0; i < n; ++i) 11 scanf("%d", &p[i]); 12 sort(p, p + n); //處理亂序輸入,適用於可重復集合 13 do { 14 for (int i = 0; i < n; ++i) 15 printf("%d ", p[i]); 16 printf("\n"); 17 } while (next_permutation(p, p + n)); //bool next_purmutation(begin, end) 18 system("pause"); 19 return 0; 20 }
遞歸生成1-n的全排列(無重復元素):如abc的全排列有abc,acb,bac,bca,cba,cab,一共3!個。
設對R={r1,r2,......rn}全排列,設Ri = R - {ri},perm(X)表示集合X的全排序,(ri)perm(X)表示X全排序前加前綴ri
全排列的遞歸定義:設n是集合中元素個數
r n = 1
perm(R)= (r1)perm(R1)
......
(rn)perm(Rn)
如果給定集合{a,b,c,d},所有可能的排列由下列排列組成
1.以a開頭的后面跟着(b,c,d)的所有排列
2.以b開頭的后面跟着(a,c,d)的所有排列
3.以c開頭的后面跟着(a,b,d)的所有排列
4.以d開頭的后面跟着(a,b,c)的所有排列
遞歸線索是“后面跟着。。。的所有排列”,若能夠解決n-1個元素集合的排列,就可以解決n個元素集合的排列問題。
處理時將所有數分別與第一個數交換,這樣就總是在處理后n-1個數的全排列。
1 #include <iostream> 2 #include <cstdio> 3 #include <algorithm> //for swap(x,y) 4 using namespace std; 5 void perm(char *a, int i, int n) 6 { 7 if (i == n) //遞歸出口 8 { 9 for (int j = 0; j <= n; ++j) 10 printf("%c", a[j]); 11 printf("\n"); 12 } 13 else 14 { 15 for (int j = i; j <= n; ++j) 16 { 17 swap(a[i], a[j]); 18 perm(a, i + 1, n); 19 swap(a[i], a[j]); //第二個swap用於恢復與第一個數交換 20 } 21 } 22 } 23 int main() 24 { 25 char a[3] = {'a', 'b', 'c'}; 26 perm(a, 0, 2); //初始調用perm(list, 0, n-1) 27 system("pause"); 28 return 0; 29 }
執行后輸出abc,acb,bac,bca,cba,cab,按照字典序輸出。
根據這種思路還有一種方法確定遞歸函數:(參考劉汝佳《算法競賽入門經典》)
1已經確定的前綴序列,以便輸出
2需要進行全排列的元素集合,以便依次選作第一個元素
偽代碼:
void perm(序列A, 集合S)
{
if(S為空)
輸出序列A;
else 按照從小到大的順序依次考慮S的每個元素v
perm(在A的末尾添加v后得到的新序列,S-{v});
}
代碼如下:
1 #include <iostream> 2 #include <cstdio> 3 using namespace std; 4 void perm(int n, int *a, int cur) 5 { 6 int i, j; 7 if (cur == n) //遞歸邊界 8 { 9 for (i = 0; i < n; ++i) 10 printf("%d", a[i]); //處理字母時只需要改成printf("%c", a[i]+'a'-1); 11 printf("\n"); 12 } 13 else for (i = 1; i <= n; ++i) //在a[cur]中填各種整數i 14 { 15 bool ok = 1; 16 for (j = 0; j < cur; ++j) 17 if (a[j] == i) 18 ok = 0; //若i在a[0]...a[cur-1]出現過,則不能再選 19 if (ok) //ok為真說明i沒有在序列中出現過 20 { //就把i添加到序列末尾后遞歸調用,這樣處理就不必保存集合S了 21 a[cur] = i; 22 perm(n, a, cur + 1); 23 } 24 } 25 } 26 int main() 27 { 28 int a[] = {1, 2, 3}; 29 perm(3, a, 0); //初始調用perm(n, a, 0) 30 system("pause"); 31 return 0; 32 }
輸出結果123,132,213,231,312,321,改成字母后輸出abc,acb,bac,bca,cba,cab。