子集生成——回溯法的准备篇
在如何获得全排列的文字里,大家会发现递归可以说随处可见,而在回溯法中,递归更是实现枚举的基本手段。在生成子集的方法中,我们也将看到递归的影子
为了简单,这里不涉及可重集的子集
递归不能深究详细的过程,而要注意第一步向第二步如何推广,以及推广后的递归如何在边界终止。
一、增量构造法
这种思路和按照字典序枚举全排列的思路一致,本质是递归地构造子集。
思路等同于解答树:
- 如果子集非空,则输出
- 递归调用,将a[cur-1]+1不断赋值给a[cur],然后递归找出剩余的子集
- 边界终止条件:隐式的递归条件,当没有元素可加入时,递归就会停止
代码如下:
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?' ':'\n');//对于要求严格的题目,一定要保证输出没有多余的空格 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("\n"); 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("\n"); 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 }
其实我觉得我对于二进制还不是很熟悉,因此二进制法应当继续加深练习。今天就到这,拜拜~~~