子集和问题 —— 一种组合生成算法

作者: JohnWaken
邮箱: JohnWaken@163.com
转载请著明: http://www.cnblogs.com/john-d/admin/EditPosts.aspx?postid=1638411


今天在网上看见这么一道题目:给你m个数,从里面找出和为sum的n个数,问一共能找到多少组这样的数。

根据我的理解,这是一道组合生成的题目。令m个数组成的集合为M,就是要找到所有元素个数为n且和为sum的子集。
最笨的方法是生成所有的子集,然后进行验证,这样复杂度为阶乘。显然有一种改进的算法,在笨方法中,我们连元素个数不为n的子集都生成了,而这显然是不必要的。这种改进想到很容易,但实现起来有点困难,经过数个小时的艰苦奋战,我终于设计了一种原创性的组合生成算法,虽然它应该早被计算机科学家想到了,但我还是抑制不住激动。

我们知道元素个数为m的几何,它的大小为n的子集的个数肯定是C(m,n),由于博客园不能用Tex写公式,我只能这样表示组合公式。理想的算法应该是只生成这些子集,这谁都知道,关键是怎么通过一种可列的方式来生成它们。

我们用一个二进制数来表示集合,用一个二进制位来表示元素,相应位是1则说明集合包含该元素,否则不包含。先考虑一个特例,集合A的大小为5,生成所有大小为3的子集。显然 00111符合条件,01011也符合条件,还有01110、01101,但是我们不能瞎猜吧。下面我画出树图,也是我的思维图。

  
上图就是5选3的结果,现在我们来分析一下怎么由父结点生成子结点,根结点可以很容易求出来,如果知道如何由父生成子,那么整个问题就解决了。我用序对(p,q)表示将第p位变为1,第q位变为0,注意下标从0开始。请检查下列序对是否有00111生成了他的5个儿子。
(3,0)
(3,1)
(3,2)
(4,1)
(4,2)

请认真验证上面的操作,有些东西是无法用语言来表达的,相信验证之后你就理解得差不多了。对,红色的位置是分界线,p从红色的位置开始递增,而q是从0递增到红色位置的前面一位,反映在程序中应该是个二层循环:外层是p在递增,内层是q在递增。

再看01110是如何生成它的两个儿子的?
(4,1)
(4,2)
开始p的起始值是3,现在成了4;开始q的起始值是0,现在成了1。随着树的层数递增,两个起始值就会递增。

我只能说这么多了,实在太难表达了。

OK,上通用算法吧。

generate.c
1 void generate(int m, int n)
2 {
3 //计算根,这里用了位的技巧
4   int num = (1<<n)-1;
5 //根据根来生成所有的子集
6   gen(num,m, n, n, 0);
7 }
8
9  /*
10 * num: 父亲
11 * m: 原集合的元素个数
12 * n: 生成集合的元素个数
13 * p: 分界,即树中的红色位置
14 * q: 右边界,作边界始终是m-1
15 */
16 void gen(int num, int m, int n, int p, int q)
17 {
18 int i, j;
19 int temp;
20 /* 这个函数看具体应用 */
21 process(num);
22
23 /* 递归终止条件非常重要 */
24 if (p>=m || q==n) return;
25
26 for (i=p; i<m; i++) {
27 for (j=q; j<n; j++) {
28 /*
29 *由num生成temp,
30 *也就是儿子。
31 *这里也用了些位的技巧
32 */
33 temp = num;
34 temp -= (1<<j);
35 temp += (1<<i);
36 /*以该结点为父亲寻找儿子 */
37 gen(temp, m, n, i+1, j+1);
38 }
39 }
40 }


如果你发现文中有错误,请在此博客留言或通过email联系我。

PS:哎,代码和公式编辑始终是个头疼的问题。

posted on 2010-01-03 21:02  John Waken  阅读(7350)  评论(3编辑  收藏  举报

导航