括号的组合问题 --> 一种关于卡特兰数的组合的算法

前言:前天吃晚饭的时候,在去食堂的路上,同学给我们出了一个括号组合的问题。虽然利用吃饭的时间,给出了合理的计算雏形,但是其中很多的细节,在没有用纸笔的情况下,是分析不全的。回到实验室后,他告诉我们这是一个求解卡特兰树的问题。可以直接用公式做。好吧,这是我第一次听到数学家-卡特兰的名字。虽然被告诉了用公式很快就能计算出结果,但是作为一个有些偏执的小白程序员,我还是决定用我晚饭间想到的方式实现,并解决这个问题。白天琐事太多,没得机会,晚上脑子又转的慢,所以磨磨唧唧的隔了两天才搞定。现在写点东西总结一下收获。 写了个我感觉有点糙的python代码放在个人空间了,暂时就不贴在这里了。


原问题:现在有左括号'(' 和右括号')' 若干,当然数量上左右匹配。问对于总数为N的情况下,共有多少种排列组合?

>>> 如果将单个括号当做处理对象的话,那么最简单直观的方法就是用二叉树了:

 首先构造一个 节点数为2^N-1的完全二叉树, 这个树具有这样的特点,每个节点的数据只存一个整型数,int flag,并且flag的取值只为-1和1,而根节点的值为1。1代表'(' ,-1代表')'。

当构造完后,需要定义三个全局变量,int deep,count,threshold,分别代表当前遍历节点所在的深度,已经获得的有些结果的统计值,和一个遍历时判断当前路径是否有效的判别值。

按照根-左-右的顺序遍历这个树。

对于threshold这个判别值,从根节点开始,在遍历的时候,尝试和将当前节点的flag相加,如果结果为负,则说明当前节点无效,向上返回。

对于count,只有当deep的值为N,既遍历到叶子节点时(并且此时的threshold应该为0),说明找到了一条合理路径,即一个合理的组合,count+1。


但是这样的话,需要产生很多没有意义的节点。如果不考虑更巧妙的算法,只考虑在生成合法的结果(组合)的情况下计算组合数的话,那么可以考虑这样:

>>> 首先,我们以一个括号对 ‘()’ 当做一个处理对象的话,那么进一步可以将这个括号对视为一个树(森林)的节点。那么,对于题目,合理的转化就是:括号内嵌括号<-->父节点下面有子节点。那么题目就变成了给定M个节点,能构成多少个树(森林)。进一步的由于按照约定的方法,一个树(森林)可以与一个二叉树唯一对应,那么题目也可以变为给定M个节点,能够构成多少个二叉树。

OK,题目先拓展到这,下面接着说说操作细节。

首先,对于输入的N个节点,我们定义以一个N-1位的序列表示一种合法的组合,或者说是一种合法的树(森林)的结构。


例如 :           0                   0            对于这个森林,N=8,我们从第0个树开始,

                    /   |   \                |            按照先根的顺序遍历每个树的每个节点。

                 0    0    0            0            并记录当前节点的子节点数。 

                      /   \                                则我们可以用 [ 3, 0, 2, 0, 0, 0, 1, 0 ] 表示这个森林。

                   0       0

说明了单个组合结果的表示方式,下面我们来讲如何生成有效的组合结果。

我们需要一个list,用这个list来存储我们生成的序列/组合。首先,根据输入的N,为list加入初始数据[0], [1],..[N-1]。因为我们最终的组合结果是N位的序列,所以我们需要进行N-1次循环,来为这些初始数据的后续位生成数据。

进行第n次循环时,逐个取下n-1次完成的序列。然后对n-1次生成的序列进行诸位遍历:遍历的过程中统计两个数据:已经使用的节点数 used(0),预约使用的节点数preUsed(0)。

以上面的例子为例, 假如我们要为序列mem = [3,0,2,0,0,0,1,0] 中索引为2的位置生成数据,假设现在mem = [ 3, 0 ] 。那么开始遍历mem:mem[0] = 3,我们已经用一个节点制造了第0个树的根节点,那么已使用的节点数 used += 1 ( 现在为1),并且这个节点预定了三个节点用来生成他的子节点,所以 preUsed += 3,所以在为序列的下一位生成数据时,即为 mem[1] 生成数据时,可用的节点数为 N-used-preUsed = 8 - 1 - 3 = 4。因为我们已经假定是在为mem[2] 生成数据,那么还得继续遍历。遍历 mem[1] = 0,我们又用了一个节点用来生成 mem[1] 所代表的节点,但是preUsed > 0,即说明 这个节点是被之前的某节点预约为子节点的,所以 used += 1 的同时, preUsed -= 1,mem[1] = 0,说明这个节点没有预定子节点,那么在为序列的下一位生成数据时,可用的节点数为 N - used - preUsed = 8 - 2 -2 = 4。遍历完了,现在为 mem[2] 生成数据,因为 preUsed > 0, 说明当前构造的这个节点已经被预约了,所以不用担心生成这一节点对于未使用的节点的开销。所以对于即将构造的这个节点,未使用的4个节点都是可用的。也即对于即将构造的这个节点,其子节点范围为 0,1,2,3,4 ,都是合法的取值,然后将这个五个可取值添加到mem上,生成五个新的mem,原来的mem取下来,在处理后舍弃,将新生成的mem加到用来存储组合的list上,代表处理完了一个位,本次循环结束。   其他例子就不多说了,值得注意的是:当 preUsed 为 0 时,说明当前没有被预约的节点,那么在为序列的下一位生成数据时,首先要preUsed += 1,即为即将构造的节点预约一个用来生成自己的节点。

上面的算法,按照我们先前的约定,可以计算出对于给定的一个N,可以生成的数(森林)的所有结构。进而统计出能构造出的树(森林)的数量,也就是括号的组合数量,二叉树的生成数量,出入栈的组合数量。

这里简答解释一下,如果上述序列如何表示一个出入栈的情况,首先,按照上面的那个森林的例子,按照给定的序列,先遍历/生成出数(森林)。例如有 ABCDE :[ 3, 0, 1, 0, 0] 

                A  

            /    |   \                    还是按照 先根顺序遍历这个树,A入栈, B入栈,B出栈,C入栈,

         B     C   D                E入栈, E出栈,C出栈, D入栈,D出栈, A出栈。

                |                         即从根节点开始入栈,从叶节点返回时,叶节点出栈,

               E                          当访问完某节点的所有子节点时,该节点出栈。


OK,就这么点内容了,欢迎大家批评我的代码(没有贴在这里,在个人空间里),给出建设性的意见。

最后,赞一下大数学家-卡特兰!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值