1 增量构造法
第一种思路是{ 递归、一次选出一个元素放到集合中 },程序如下:
void print_subset(int n, int* A, int cur) {
for(int i = 0; i < cur; i++) printf("%d ", A[i]); //打印当前集合
printf("\n");
int s = cur ? A[cur-1]+1 : 0; //确定当前元素的最小可能值
for(int i = s; i < n; i++) {
A[cur] = i;
print_subset(n, A, cur+1); //递归构造子集
}
}
和前面不同,由于A中的元素个数不确定,每次递归调用都要输出当前集合。
另外,递归边界也不需要显式确定——如果无法继续添加元素,自然就不会再递归了。
上面的代码用到了定序的技巧:规定集合A中元素编号从小到大排列,就不会把集合{1, 2}重复输出了。
提示:在枚举子集的增量法中,需要使用定序的技巧,避免同一个集合枚举两次。
这棵解答树上有2^n个结点。因为每个可能的A都对应一个结点,而n元素集合恰好有2^n个子集。
2 位向量法
第二种思路是{ 构造一个位向量B[i]、递归 },而不是直接构造子集A本身,其中 : B[i]=1,当且仅当i在子集A中。
void print_subset(int n, int* B, int cur) { if(cur == n) { for(int i = 0; i < cur; i++) if(B[i]) printf("%d ", i); //打印当前集合 printf("\n"); return; } B[cur] = 1; //选第cur个元素 print_subset(n, B, cur+1); B[cur] = 0; //不选第cur个元素 print_subset(n, B, cur+1); }
必须当“所有元素是否选择”全部确定完毕后才是一个完整的子集,因此仍然像以前那样当if(cur == n)成立时才输出。
现在的解答树上有2^(n+1)-1个结点,比刚才的方法略多。
这个也不难理解:所有部分解(不完整的解)也对应着解答树上的结点。
提示:在枚举子集的位向量法中,解答树的结点数略多,但在多数情况下仍然够快。
这是一棵n+1层的二叉树(cur的范围从0~n),第0层有1个结点,第1层有2个结点,第2层有4个结点,第3层有8个结点,……,第i层有2i个结点,总数为1+2+4+8+…+2^n=2^(n+1)-1,和实验结果一致。
这棵树依然符合前面的观察结果:最后几层结点数占整棵树的绝大多数。
7.3.3 二进制法
另外,还可以用二进制来表示{0, 1, 2,…,n-1}的子集S:从右往左第i位(各位从0开始编号)表示元素i是否在集合S中。
注意:为了处理方便,最右边的位总是对应元素0,而不是元素1。
提示:可以用二进制表示子集,其中从右往左第i位(从0开始编号)表示元素i是否在集合中(1表示“在”,0表示“不在”)。
此时仅表示出集合是不够的,还需要对集合进行操作。幸运的是,常见的集合运算都可以用位运算符简单实现。
最常见的二元位运算是与(&)、或(|)、非(!),它们和对应的逻辑运算非常相似.
“异或(XOR)”运算符“^”,其规则是“如果A和B不相同,则A^B为1,否则为0”。
异或运算最重要的性质就是“开关性”——异或两次以后相当于没有异或,即A^B^B=A。
另外,与、或和异或都满足交换律:A&B=B&A,A|B=B|A,A^B=B^A。
与逻辑运算符不同的是,位运算符是逐位进行的——两个32位整数的“按位与”相当于32对0/1值之间的运算。
不难看出,A&B、A|B和A^B分别对应集合的交、并和对称差。另外,空集为0,全集{0, 1, 2,…, n-1}的二进制为n个1,即十进制的2n-1。为了方便,往往在程序中把全集定义为ALL_BITS= (1<<n)-1,则A的补集就是ALL_BITS^A。当然,直接用整数减法ALL_BITS -A也可以,但速度比位运算“^”慢。
提示:当用二进制表示子集时,位运算中的按位与、或、异或对应集合的交、并和对称差。
这样,不难用下面的程序输出子集S对应的各个元素:
void print_subset(int n, int s) { //打印{0, 1, 2,..., n-1}的子集S for(int i = 0; i < n; i++) if(s&(1<<i)) printf("%d ", i); //这里利用了C语言"非0值都为真"的规定 printf("\n"); }
而枚举子集和枚举整数一样简单:
for(int i = 0; i < (1<<n); i++) //枚举各子集所对应的编码0, 1, 2,..., 2^n-1 print_subset(n, i);
提示:从代码量看,枚举子集的最简单方法是二进制法。