给定一个集合,枚举所有可能的子集,为了简单起见,假设集合中的元素不重复。则 n n n 个元素的集合会有 2 n 2^n 2n 个子集,而且不相同。
下列算法均以生成 {1,2,…,n}的子集为例。
void subset1(int* A, int n,int cur) {
/* n为原集合的元素个数,cur为当前生成集合的长度*/
for (int i = 0; i < cur; i++) printf("%d ", A[i]);
printf("\n"); // 先打印已有集合
int s = cur ? A[cur - 1] + 1 : 1; // 确定下一可选元素的最小值
for (int i = s; i <= n; i++) {
A[cur] = i;// 从1开始
subset1(A, n, cur + 1); // 递归构造子集
}
}
{1,2}
就不会枚举{2,1}
了。第二种方法是不直接构造子集A本身,而是构造一个位向量vis[]
,即 v i s [ i ] = 1 , 当 i 在 子 集 A 中 ; 否 则 v i s [ i ] = 0 vis[i] = 1,当i在子集A中;否则vis[i] = 0 vis[i]=1,当i在子集A中;否则vis[i]=0,则对于n个元素的集合,我们需要构造长度为n的位向量。
void subset2(int* vis, int n, int cur) {
if (cur == n) {
for (int i = 0; i < n; i++) {
if (vis[i]) cout << i + 1 << " ";
}
cout << endl;
return;
}
vis[cur] = 1; // 包含该元素
subset2(vis, n, cur + 1);
vis[cur] = 0; // 不包含该元素
subset2(vis, n, cur + 1);
return;
}
在位向量法 种我们用一个位向量来表示最终的结果,由于一个位置的取值只会是0或1,则我们可以用二进制来表示我们的结果。 即对于集合 S = { 0 , 1 , 2 , 3 , . . . , n − 1 } S = \{0,1,2,3,...,n-1\} S={0,1,2,3,...,n−1},我们用一个二进制数从右往左第 i i i 位表示元素 i i i是否在集合S中(个位从0开始编号)。
例如,下图用二进制 0100 − 0110 − 0011 − 0111 0100-0110-0011-0111 0100−0110−0011−0111 来表示集合 { 0 , 1 , 2 , 4 , 5 , 9 , 10 , 14 } \{0,1,2,4,5,9,10,14\} {0,1,2,4,5,9,10,14}
注意:为了处理方便,最右面的元素编号为0
使用二进制表示子集有一个很大的好处,即集合间的操作能够用二进制的位操作来替换。
C语言中常见的二元位运算与(&)、或(|)、非(!)、异或(^)的真值表:
其中 与1进行异或相等于取相反数,即异或的规则是相同为0,不同为1。且 0^1=1,1^1=0
特别地,对于有n个元素的全集我们定义为 ALL = (1<
A的补集定义为ALL ^ A
,而不是非运算。
而代码实现看起来非常的简洁
void print_subset3(int n, int s) {
/* n为位向量长度,s为该位向量 */
for (int i = 0; i < n; i++) {
if (s & (1 << i)) printf("%d ", i);
}
printf("\n");
}
// 二进制法枚举
void subset3(int n) {
for (int i = 0; i < (1 << n); i++)// 构造位向量
print_subset3(n, i);
}
参考资料
《算法竞赛入门 第二版》