【暖*墟】#子集生成# 的方法

给定一个集合, 枚举所有可能的子集。为了简单起见,本节讨论的集合中 没有重复元素。

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);

提示:从代码量看,枚举子集的最简单方法是二进制法。

猜你喜欢

转载自blog.csdn.net/flora715/article/details/80968053
今日推荐