要求:
給定一個集合,枚舉所有可能的子集。此處的集合是不包含重復元素的。
Method0: 增量構造法
思路:每次選取一個元素至集合中,為了避免枚舉重復的集合,此處要采用定序技巧 -- 除了第一個元素,每次選取必須要比集合中的前一個元素要大!
// A 為原集合; // B 為子集,每次調用函數即會打印一次 // cur 為子集元素個數 void print_subset0(int *A, int *B, int N, int cur) { for(int i=0; i<cur; i++) { printf("%5d", B[i]); } printf("\n"); if( cur < N ) { for( int i=0; i<N; i++ ) { if( !cur || A[i] > B[cur-1] ) { B[cur] = A[i]; print_subset0(A, B, N, cur+1); } } } } int main() { int Length = 3; int A[Length] = {1, 3, 2}; printf("Method0:\n"); int B[Length] = {0}; print_subset0(A, B, Length, 0); printf("\n"); return 0; }
采用的是遞歸調用,但此處不需要return語句,因為當沒有元素可用於枚舉時,就不會調用函數,也就是不會繼續遞歸。
此函數輸出的子集中是包含空集的,如果不想用空集,則需判斷 cur 是否為0,不為0才打印子集
測試樣例的輸出結果(包含空集):
Method1: 位向量法
思路:1個容量為N的集合,每個位置0~N-1,對於每個子集,要么被選中,要么沒被選中。枚舉每一個位置的狀態,可得到各種子集。
// A 為原集合; // A 為原集合 // used為當前A中每個位置的元素的狀態(選中或未被選中) // cur代表現在枚舉A[cur]的狀態 void print_subset1(int *A, int *used, int N, int cur) { if( cur == N ) { for(int i=0; i<N; i++) { if( used[i] ) { printf("%5d", A[i]); } } printf("\n"); return ; } used[cur] = 0; print_subset1(A, used, N, cur+1); used[cur] = 1; print_subset1(A, used, N, cur+1); } int main() { int Length = 3; int A[Length] = {1, 3, 2}; printf("Method1:\n"); int B[Length] = {0}; print_subset1(A, B, Length, 0); printf("\n"); return 0; }
同樣是遞歸枚舉,這里需要用return終止遞歸,終止條件就是cur == N即枚舉了一種子集,然后輸出
此函數的輸出是包含空集的,如果不想要空集,則需要判斷used函數是否全為0,如果全為0,則不輸出
樣例輸出(包含空集):
Method10: 二進制法
類似於位向量法,同樣也是枚舉各個位置的狀態,但這次用二進制表示,二進制長度為N,與原集合大小相同。二進制的第 i 位代表原集合中的第 i 位是否被選中,枚舉各種情況。集合大小為N,就是2的N次種方式。
void print_subset10(int *A, int N, int seq) { for(int i=0; i<N; i++) { if( seq & (1<<i) ) { printf("%5d", A[i]); } } printf("\n"); } int main() { int Length = 3; int A[Length] = {1, 3, 2}; printf("Method10:\n"); for(int i=0; i<(1<<Length); i++) { print_subset10(A, Length, i); } printf("\n"); return 0; }
這種方式很好寫,也很好記,但問題是,因為函數中的形參seq是int型的,所以N最大也就只能32,如果long long,那N也只能最大64,再超過64,就需要用大數或其它表示方式表示了。
如果不想要空集,可以將main函數中的 i 從1枚舉起。
樣例輸出結果(包含空集):
參考資料: 《算法競賽入門經典(第2版)》