摘要
刚刚完成一篇利用位运算高效地、巧妙地来解决求组合的博文:《非常给力:位运算求组合》。巧合的是,我在《数据结构算法与应用》一书中看到一道课后题是:用递归实现求一个集合的所有子集。受到题目的要求,我开始想递归,想着想着,我就发现此题不用递归而用位运算来求解,仍然非常巧妙!本篇,我将讲解如何利用位运算来求解集合的所有子集,个人认为这种方法简单、易懂,而且高效!欲知详情,请看下文:
前奏
最近一直在关注位运算及其应用,并完成了多篇博文,其中《非常给力:位运算求组合》是一篇非常值得阅读的一篇,它是我将POJ上的一道水题跟求组合的问题结合而成的。在此力荐各位朋友看看那篇博文。一方面,我正是在完成那篇博文的基础上想到本篇即将要介绍的求子集的方法的,可以说要是没有撰写那篇博文的经历,我根本无法想到本篇的方法;另一方面,本篇应用了那篇博文的很多知识点,这些东西由于在那篇博文中已经介绍过,因此本篇直接利用之,如果您阅读时有不解之处,那么请先阅读那篇博文。
入题
编写算法求一个集合的所有子集,比如集合set={a,b,c,d}的所有子集是:
{},
{a}, {b}, {c}, {d},
{a, b}, {a, c}, {a, d}, {b, c}, {b, d}, {c, d},
{a, b, c}, {a, b, d}, {a, c, d}, {b, c, d},
{a, b, c, d}
一共16个,事实上n个元素的集合的子集共有2n个(包含空集)。
攀亲
事实上“求集合的子集”跟“求组合”是亲缘关系。为什么呢?首先,集合set={s1,s2,……sn}的所有子集可以看做是由多组子集组成的,这些集合组是:
包含0个元素的子集(空集)
包含1个元素的子集
……
包含n个元素的子集。
其次,“包含1个元素的子集”与“从set中选择1个元素的组合”等价,“包含2个元素的子集”与“从set中选择2个元素的组合”等价……“包含n个元素的子集”与“从set中选择n个元素的组合”等价!
因此,求set的所有子集的问题就化成了分别求从set中选择1,2……n个元素的组合的问题!注意这里我们并没考虑空子集,因为任何集合都包含且仅包含一个空子集,因此可以不用考虑它。
还记得上一篇吗,它的主题就是求组合,其核心是位运算!因此,本篇也是上一篇的姊妹篇。这里再次提醒,如果你没有看上一篇《非常给力:位运算求组合》,那么你可能会在阅读本篇中遇到麻烦,由于上一篇我讲解得比较细致(个人认为),特别是那个至关重要的引例(利用位操作实现求最小的比N大的,二进制表示中1的个数跟N相同的数),因此,本篇对这些重复的内容不再讲解,而是直接拿来应用。下面是上一篇的核心程序,同时它也是本篇的核心:
int NextN(int N) { return (N+(N&(-N))) | ((N^(N+(N&(-N))))/(N&(-N)))>>2; }
该代码实现的功能是:求得最小的比N大的,二进制表示中1的个数跟N相同的数!如果你对该代码有任何疑问,那么请您回过头去看《非常给力:位运算求组合》一文,那里有对该代码的详细剖析,我个人强烈推荐它,如果您在阅读它遇到任何问题,或者发觉任何不对之处,请与我联系,联系方式是:[email protected]。
思路&实现
现在开始介绍利用位操作来求一个集合的所有子集。用语言描述这个过程就是:用一个整数的二进制位来标识集合中的某个元素是否包含在某个子集内。我们的任务是要求得set的包含i个元素的子集(组合),其中i取值1,2,……n。这里假设set={a, b, c, d};那么下面四个数对应着set的四个只包含1个元素的子集:
00000001 : {a}
00000010 : {b} //0000 0010 = NextN(00000001)
00000100 : {c} //0000 0100 = NextN(00000010)
00001000 : {d} //0000 1000 = NextN(00000100)
同理可得,下面的6个数,分别与set的6个只包含2个元素的子集对应
00000011: {a, b}
00000101: {a, c}
00000110: {b, c}
00001001: {a, d}
00001010: {b, d}
00001100: {c, d}
由上可知,我们只需要根据每组的第一个数(上面标粗加红的数字)就能利用前面那个NextN函数求得该组后面的所有的数,这样就能求得其对应的子集。通过分析,我们得知每一组的第一个数字(上面标粗加红的数字)必然是(1<<i) -1;比如说与set的只包含1个元素的子集对应的第一个数字是0000 0001 = (1<<1) -1,与set的只包含2个元素的子集对应的第一个数字是0000 0011 = (1<<2) -1。有了每一组的第一个数字,我们利用NextN函数就能求得该组的其他数字,当求出一个数字之后,打印出该数字所对应的组合。只要打印出每一组的所有数字对应的组合,我们就能求得集合set的子集,下面是求集合set的子集的步骤(其中假设集合set包含n个元素):
1、 定义i= 1; //表示当前输出包含i个元素的所有子集
2、 判断i是否小于n,如果是,则转3,否则转7。
3、 定义c = (1<<i)-1; //c是所有包含i个元素的子集中的第一个子集
4、 判断c是否小于等于(1<<n)-1,如果是则转5,否则转6。//最后一个子集包含所有元素,其对应的数字是(1<<n)-1
5、 调用print(set,c)打印set的与当前c对应的子集,并用c=NextN(c)计算下一个子集对应的数字,然后转4。
6、 执行i++;然后转2
7、 结束
从上面的步骤可以看出,该程序是个两重循环结构,其流程图如下:
在上面的流程中用于打印set的与数字c对应的子集的函数也是我在博文《非常给力:位运算求组合》中实现过的,其代码如下:
void print(char* set,int C) { int i = 0; int k; while((k=1<<i)<=C) {//循环测试每个bit是否为1 if((C&k)!=0) { cout<<set[i]; } i++; } }
然后根据上述流程图,写出求集合set的所有子集(不包含空集)的函数如下:
//求set的所有非空子集,n是set中包含的元素个数 void SubSet(char* set,int n) { int i =1; while (i<=n) { int c = (1<<i) -1; //c是每一组的第一个数 while (c<=(1<<n)-1) { print(set,c); cout<<endl; c = NextN(c); } i++; } }
最后写一个用于测试的程序:
char set[4] = {'a','b','c','d'}; void main() { SubSet(set,4); }
该程序在VC6.0上测试通过,其运行结果如下图:
分析
如前面的图所示,实现了set的所有子集的输出(当然不包含空子集,如果需要你可以随时加上,同时输出格式并不是{a, b, c}的形式,这些问题都很好解决,这里不赘述了)。
通过分析,该方法跟《非常给力:位运算求组合》一文中的求组合一样有其使用限制:1、当集合中元素个数较多时,无法使用该方法,因为你无法找到足够大的整数来标识每一个元素。
2、顺序问题:原题要求输出{a, c}之后输出{a, d},然而我的程序输出的却是{b, c},原因跟《非常给力:位运算求组合》一文一样。
结束语
到此本篇即将结束,如果您有不清楚的地方或者发现有不妥之处,请与本人联系,最后感谢您的阅读!最后预告我的下一篇即将完成的关于位运算博文《位操作应用之位排序》,我计划面向对位排序了解不多的读者来写,由浅入深。并且对用标准库的bitset类来完成位排序进行讲解,提出其中的问题,最后自己实现一个简化版的bitset类,用于位排序。
申明
本人的所有原著博文的版权均归本人和CSDN所有。除侵权行为外,欢迎各位朋友评论指正、转载分享,分享时请标明作者和出处,本人昵称:语过添情(w57w57w57)。其中语过添情”是我一直用的QQ昵称,从2001年至今,它跟我已经10个年头了。后面那一串古怪的字符的来历是:字母w和数字5分别是本人姓(伍)的拼音首字母和谐音数字,7是因为我的启蒙女友(初一时的,非初恋女友,因为那时我完全不懂恋爱)的姓(柒)的谐音数字。以前天真的以为以后生个小孩可以取名伍陆柒(567)的,可惜一学期后她就辍学了……
作者: 语过添情 (w57w57w57)
Email: [email protected]
QQ: 11335457
2011-08-03