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

作者: 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,但是我们不能瞎猜吧。下面我画出树图,也是我的思维图。

   2010010320354823.png
上图就是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,上通用算法吧。

ContractedBlock.gif ExpandedBlockStart.gif 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:哎,代码和公式编辑始终是个头疼的问题。

转载于:https://www.cnblogs.com/john-d/archive/2010/01/03/1638411.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值