要求:
給定一個集合,枚舉所有可能的子集。此處的集合是不包含重復元素的。
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版)》
