子集生成——回溯法的准备篇

在如何获得全排列的文字里,大家会发现递归可以说随处可见,而在回溯法中,递归更是实现枚举的基本手段。在生成子集的方法中,我们也将看到递归的影子

为了简单,这里不涉及可重集的子集

递归不能深究详细的过程,而要注意第一步向第二步如何推广,以及推广后的递归如何在边界终止。

一、增量构造法

这种思路和按照字典序枚举全排列的思路一致,本质是递归地构造子集。

思路等同于解答树:

  1. 如果子集非空,则输出
  2. 递归调用,将a[cur-1]+1不断赋值给a[cur],然后递归找出剩余的子集
  3. 边界终止条件隐式的递归条件,当没有元素可加入时,递归就会停止

代码如下:

 1 //定义数据
 2 int a[5];
 3 void print_subset(int n,int *a,int cur){
 4      for(int i=0;i<cur;i++)printf("%d%c",a[i],i<cur-1?' ':'
');//对于要求严格的题目,一定要保证输出没有多余的空格
 5      int s=cur?a[cur-1]+1:0;//定序技巧,避免子集重复
 6      for(int i=s;i<n;i++){
 7          a[cur]=i;//将新的元素加入子集的第cur个位置
 8          print_subset(n,a,cur+1);
 9      }
10  }
11 int main(){
12     print_subset(5,a,0);
13 }

二、位向量法

位向量法与直接构造法的区别在于数组的使用方式不同,位向量法用数组下标表示子集元素,而数组内容为1或0,表示该元素 在 或者 不在 集合内

注意,和直接构造不同,每个元素都有0和1(不在/在)两种方式,因此,位向量法的解答树节点数比直接构造法多出一倍减一个,例如直接构造法的10个元素的解答树,有2^10=1024个节点,而用位向量法,因为每个元素都有0和1两种状态,因此有2*1024-1=2047个节点(根节点都只有一个,所以不是单纯2倍关系),因此速度理论上要慢一些,但多数情况下仍然够用

位向量法需要指定显式的递归边界,因为我们不能明确知道究竟一次有多少元素会被打印。

 代码如下:

 1 int a[5];
 2 void print_subset(int n,int *a,int cur){
 3     if(cur==n){//显式的递归边界
 4         for(int i=0;i<n;i++)
 5             if(a[i])printf("%d ",i);
 6         printf("
");
 7         return;
 8     }
 9     a[cur]=1;//每个位置有0和1两种状态
10     print_subset(n,a,cur+1);
11     a[cur]=0;
12     print_subset(n,a,cur+1);
13 }

三、二进制法(最快最简单代码最少,限制是数字会爆)

本质和位向量法相同,但是由于是二进制,因此一个整数(1<<n)-1就可以代表一个全集,这种实现接近计算机底层,加上C语言自身就支持二进制的运算,如&(交集),|(并集),^(对称差和开关性)等,因此速度和代码简洁性都优于位向量法。

 二进制法从右向左表示集合中的元素(从低位到高位)如0110是数字6,代表的集合是{ 1,2 },010111是数字23,代表的是{ 0,1,2,4 },n个元素所有的可能的情况有2^n个,即(1<<n)-1种

实现如下: 

 1 void print_subset(int n,int s){//s为{0,1,。。,n-1}的子集 
 2     for(int i=0;i<n;i++)
 3         if(s&(1<<i))printf("%d ",i);
 4     if(s)printf("
");
 5 }
 6 int main(){
 7     int n=5;
 8     for(int i=0,len=1<<n;i<len;i++)//枚举子集
 9         print_subset(n,i);
10 }

其实我觉得我对于二进制还不是很熟悉,因此二进制法应当继续加深练习。今天就到这,拜拜~~~

原文地址:https://www.cnblogs.com/luruiyuan/p/5625427.html