昨天看到有人在TL里说一个面试题 : 不用递归法 求一个固定集合内的所有子集。经 @miloyip 指出,这是个power set 的问题,在中文中称为幂集。这个中学数学里就学过了,不过如何用计算机求幂集还真没考虑过,就索性找了找求幂集的算法。根据wiki上的介绍和我自己的理解( http://en.wikipedia.org/wiki/Power_set ),给定一个包含n个元素的集合s,我用python实现了三种计算幂集的算法。如果没有额外说明,下文中的 union表示取两个集合的合集。
1. 利用二进制数与幂集之间的对应关系
在[0,2^(n-1)]的整数区间上任取一个值x,x的二进制表示可以用来表示s的一个子集:对于x的第i位,如果为1,则此子集包含s的第i个元素,否则不包含。因此,只要遍历 [0,2^(n-1)]的整数区间,就能列举出s的所有子集,这些子集的集合就是s的幂集。这个方法陈硕老大也指出来了。代码如下:
def power_set(s): n = len(s) test_marks = [ 1<<i for i in range(0, n) ] for k in range(0, 2**n): l = [] for idx, item in enumerate(test_marks): if k&item: l.append(s[idx]) yield set(l) #A simple test def __test__(): s = [1,2,3,4] for e in power_set(s): print e __test__()
2. 递归计算和一个改进的非递归算法
wiki上给出了一个递归的算法,大致思想是先取出s中的一个元素e,然后求s中其他元素组成的集合的幂集,再把e与求出的幂集中的每个结合进行组合求合集,因此能列举出所有可能的子集。这个算法在列举过程中某些子集可能会重复出现,因此我没有用yield,而是直接返回了一个list。代码如下:
#simply add a set s to list l, with duplicate check def add_set_to_list(l, s): if s not in l: l.append(s) #F(e, T) = {x union {e} | x belongs to T} def F(e, T): l = [] add_set_to_list(l, e) for x in T: y = e | x add_set_to_list(l, y) return l def power_set(s): l = [] if not s: #empty set l.append(set()) return l for e in s: es = set((e,)) t = s - es s2 = power_set(t) for i in s2: add_set_to_list(l, i) for i in F(es, s2): add_set_to_list(l, i) return l
上面这个算法有两个缺点:一是计算过程中会重复生成某些集合,因此 有重复计算,二是它是递归的,效率上可能不高。经过与 @GiantIron 讨论,得到了一种非递归算法,思想跟上面的递归算法类似,但又有区别,而代码要简单得多。非递归算法的数学描述如下:
给定一个集合s,它的幂集表示为P(s), 则对于一个元素e, P( s union {e} } = P(s) union F(e, P(s))。直观地说,已知集合s和它的幂集P(s),如果往s中加入一个新元素e,则形成的新的集合的幂集是 P(s) 和 F(e, P(s)) 的合集。其中F()函数在递归算法的实现中已经给出。上述描述其实跟递归算法中类似,但是我们可以利用这个关系来增量地计算,而不用递归。
代码如下,l用来保存增量计算出来的幂集,由于后续计算依赖于之前计算的结果,这里也没有用yield,直接返回一个list。因为这个计算过程中不会出现重复的子集,所以直接用list.extend()代替了取合集(union)的操作。同样,在递归算法中的F()函数的实现中有检查是否是重复元素的操作,而在这里其实是不必要的:
def power_set(s): l = [set(),] for e in s: l.extend(F(set((e,)), l)) return l
3. s的幂集 = { subsets(s, k) | k=0, 1, 2, ..., n}
其中subsets(s,k)表示s所有模为k的子集,即 包含k个元素。这个关系是显而易见的。因此只要求出subsets(s,k),幂集也就很容易了。求subset(s,k)的代码我没有自己写,在这里 有一个实现,也是递归的,它利用了幂集与二项式系数之间的关系以及帕斯卡法则 (其实跟杨辉三角所描述的关系是一样的),显得很优美。代码如下,其中k_subsets(s,k)是别人实现的subsets(s,k)的函数。
def power_set(s): for k in range(0, len(s)+1): for k_set in k_subsets(s,k): yield k_set
除此了上述方法外,陈硕老大也介绍了用stl的next_permutaion()来生成组合数(链接 ),进而求幂集。我觉得这个方法跟上述第三种方法有相似的地方,都是利用了组合与幂集之间的关系,不过生成组合的方法不一样。我相信还有更多的方法可以解决这个问题。
解决同一个问题可以有很多种方法,而看似简单的一个问题,真正要正确地解出来也不是那么容易。由于python本身提供了对set的支持,实现幂集也轻松了很多。不过set对象本身不能作为set的元素,这一点有待推敲,我对python也不是太熟悉。