上次我在《基于N层满N叉树的排列算法》 这篇文章给大家演示了如何从一个N层满N叉树得出一个N全排列。N层满N叉树确实是一个蛮奇妙的数据结构,它又叫做完美N叉树,利用它同样可以形成N个元素的组合。
我们知道N个元素的M组合的计算公式为,Cnm = n!/(m!*(n-m)!)
例如4个元素中取3个进行组合的组合数=4/(2*1)=4,我们这次也试着从一个完美4叉树中获得它的3组合。

图一是一个完美4叉树,由于树结点太多,这里的b1,b2,b3,b4节点的子数采用的是简略方式表示(三角形表示其子树节点集合,例如图二就列出了b1的子树)。请注意,这次的完美4叉树跟 《基于N层满N叉树的排列算法》 一文中的有所不同。这次的树结点所代表的数字是分层的,也就是,顶层节点为0,b1、b2、b3、b4这四个次一层节点为1,如此下去,接着两层的节点数字分别等于其节点所在的层数。这种安排是为了组合算法而特意设置的。
好了,接下我们用深度优先的搜索策略来逐个的从这个树中找出所有的3组合。
首先看图二:

(1)首先我们获得a, b1,c1-1这三个节点。由于是3组合,所有搜索到c1-1可以返回,这时可以输出结果[0,1,2];
(2)搜索点回到b1后继续转向b1的子节点c1-2。由于c1-1已经在之前被选过,于是,我们跳过c1-2(以避免产生重复的组合),直接进入d1-2-1。由于d1-2-1能形成一个新的组合,所以d1-2-1获选。于是沿着路径[a][b1][c1-2][d1-2-1],我们有三个选中的节点(绿色表示选中,灰色表示落选,粗蓝线表示结果的路径)。于是,我们输出第二个结果[0,1,3];
(3)由于d1-2-1已经是叶结点,也就是说在b1有效的情形下,已经不可能存在另外一条路经,使得新的组合可以产生出来(请思考一下,为什么),于是我们可以把b1的颜色变为灰色,也就是说,将来的搜索不再选则b1了。如图三所示。

(4)我们的深度优先仍继续进行下去,这次到达了b1的第3个儿子c1-3。这次的情形和步骤(2)不同,步骤(2)的情形是b1被选中,而这次则不然。所以c1-3获选可以形成新的组合。于是c1-3获选。
(5)c1-3的儿子d1-3-1如同步骤(3)一样被选中。于是一个新的组合可以输出[0,2,3];
(6)在c1-4,情形同c1-2相象,所以c1-4也是落选的。不过这次已经不可能通过c1-4的子树形成新的组合了,因为已经没有足有的节点形成3组合。至此b1的所有子树搜索完毕。
(7)在这里我们先停顿一下。说回b1,这次搜索是a和b1最初被选中的条件下展开的,并且b1被设置为灰色(落选)的情形也已经处理过,接下来就将要在b2展开新的搜索。问题是如果b2的搜索同b1没有什么区别的话,我们只会得到跟b1一样的结果,这时我们所不愿意见到的。为此,我们需要改变一下进入b2搜索的入口条件——将a变成灰色(剔除),于是,我们能在一个全新的上下文(contxet)中展开新一轮的搜索(如图四所示)。

(8)这次我们很快的得到[b2][c2-1][d2-1-1]这条路经。于是输出[1,2,3]
(9)由于d2-1-1是叶结点,并且在b2为绿色(已选)的情形下,b2的子树中已经不再可能存在新的组合可供。于是b2以及它的子树的搜索完毕。
(10)对于b3,b4的情形,无论b3,b4处于什么的状态,都不可能获得新的输出组合了(请想想为什么)。于是全部搜索完毕。
我们恰好得到了期望中的4个组合输出。
这时一个非常高效的组合算法,处理得当的话能用o(1)的算法复杂性输出每一个组合。下图是性能测试的结果
在10组合3的情形下每秒输出组合数为9,401,760个,并且随着组合规模的扩大,性能丝毫不受影响,我想这是本算法的一个比较有特色的地方了(绝对的线性)。

下图是演示程序所形成的字符串5组合3

两个对称组合[20 C 5]和[20 C 15]所形成的互补花纹

我觉得能领会了组合数学和图论之间这这种巧妙联系,是研究这些算法的最大得益。学好图论,真的非常有帮助阿。
从这里下载包括测试演示程序的整个项目文件(VS2008版本的)。版权?Copy Free,不过风险自担
http://files.cnblogs.com/GlobalFamily/Combination.rar
对组合算法有兴趣的朋友可以参考CodeProject上的同类文章
http://www.codeproject.com/KB/recipes/combinations.aspx
做人要厚道,转载请注明出处。
作者Blog RoyDeng