此题目对应于 LeetCode 96
题目要求:
Given n, how many structurally unique BST's (binary search trees) that store values 1...n?
结点数为n的不同形态的二叉树一共有多少种?这是一个很常见的面试题。
从二叉树的定义来看,二叉树本质上就是一个递归的形式,左子树,右子树,根节点。所以根节点应该不变,需要递归处理的是左右子树。
也就是说,还是考虑固定一个节点,即根节点。好的,按照这个思路,还剩2个节点,那么左右子树的分布情况为2=0+2=1+1=2+0。
所以有3个节点时,递归形式为h(3)=h(2) + h(1)*h(1) + h(2). (注意这里的乘法,因为左右子树一起组成整棵树,根据排列组合里面的乘法原理即可得出)
那么有n个节点呢?我们固定一个节点,那么左右子树的分布情况为n-1=n-1 + 0 = n-2 + 1 = ... = 1 + n-2 = 0 + n-1
OK。递归表达式出来了h(n) = h(n-1) + h(n-2)h(1) + h(n-3)h(2) + ... + h(1)h(n-2) + h(n-1)
再定义h(0)=1时,这个递推式就表示了组合数学中经典的Catalan数:
此递推式的解为:
这里我们给出这个递推式的解法,里面会包含一些数学知识和理论推导。建议博友们可以看看,如不感兴趣的可以自行跳过。
其中
最后附上Python求Catalan数的程序。
解法1:
def Catalan(n):
if n == 0 or n == 1:
return 1
else:
result = 0
for i in range(n):
result += Catalan(i)*Catalan(n-1-i)
return result
上面的代码时粗暴的解法,提交LeetCode结果是运行超时
下面提供一份用于动态规划思想做的方法:
解法2:
class Solution(object):
def __init__(self,n):
self.dic = {} # 记录表,记录numTrees(i)的值,下次需要直接读取
def numTrees(self, n):
if n == 0 or n == 1:
self.dic[n]=1
return 1
else:
result = 0
for i in range(n):
if i in self.dic:
tmp1 = self.dic[i]
else:
tmp1 = self.numTrees(i)
if n-1-i in self.dic:
tmp2 = self.dic[n-1-i]
else:
tmp2 = self.numTrees(n-1-i)
result += tmp1*tmp2
self.dic[n] = result
return result
解法3:
由于这个问题有着通项公式,所以可以直接通过计算组合数的方法进行求解:
有公式f(n)=1/(n+1) * C(n, 2n) ,可知我们只需求得组合数C(n, 2n)。
# 从n个数中选择m个数的组合数C(n,m)
class Solution(object):
def combination(self,n,m):
tmp = 1
if m > n-m: #取n和m-n的最小值,以减少计算量
return self.combination(n,n-m)
for i in range(n-m+1):
tmp *= n-i
tmp /= i+1
return tmp
def numTrees(self, n):
return self.combination(2*n,n)/(n+1)
Catalan数的常见应用由如下几种,这里分别给出分析和详解。
应用1:结点数为n的不同形态的二叉树一共有多少种?
这个解法已经在上面的内容进行叙述。
应用2:n对括号有多少种有效匹配方式,即左右括号正确匹配?
我们先举几个例子
1对括号:() 1种可能
2对括号:()() (()) 2种可能
3对括号:((())) ()(()) ()()() (())() (()()) 5种可能
通过数列的这前几项,我们可以猜测这个数列是Catalan数,然而这里根据意义推导出递推式比较难以想象和说明,这里针对这个括号匹配的应用给出另一种表达式的推导。
(1)考虑n对括号,共有n个左括号“(”和右括号“)”。这n对左右括号的全排列,可以看做是2n个空,将n个左括号放入任意的n个空之中,剩下的位置由右括号填充。其排列数显然为C(n,2n)
(2)在所有的排列中,显然有部分是非法排列,我们从中减去非法排列个数,即可以得到合法的排列数目,现在考虑非法排列的个数。
(3)先来观察非法排列的特性,我们假设左括号“(”为1,右括号“)”为-1,非法排列有这样一个性质,对于任意一个非法排列a1,a2 ... an ,比存在一个k,使得 a1+a2+a3..ak<0。这里的想法是,假设左右括号可以这样匹配()进行抵消,这里非法的条件便是从左边出现了一个无法被抵消的右括号“)”。这里给出n=3的一个例子:
()())(,即1,-1,1,-1,-1,1,。
在k=5时出现了非法的情况。我们将1-5元素进行翻转即1和-1进行置换,该序列就变成了
-1,1,-1,1,1,这个翻转后的序列中有n+1个1,n-1个-1,共有C(n+1,2n)。这里要论证的是:
对于这种有n+1个1,n-1个-1的排列,总是存在一个最小的k,我们只需要从第1个到第k个元素翻转回去,就能变成对于有n个1,n个-1的情况下的非法排列。即有n+1个1,n-1个-1的排列与非法队列是一一对应的。
(4)所以可以推得,非法排列的个数为C(n+1,2n),最终可得结论:
对于n对括号,合法的排列共有C(n,2n) - C(n+1,2n)种.
而这就是Catalan数的另一种求解式。
**应用3:矩阵链乘: P=a1×a2×a3×……×an,依据乘法结合律,不改变其顺序,只用括号表示成对的乘积,试问有几种括号化的方案?
我们先举几个例子
1个矩阵: a1 1种方案
2个矩阵连乘: a1×a2 1种方案
3个矩阵连乘: (a1×a2)×a3 a1×(a2×a3) 2种方案
n个矩阵连乘: a1×a2×a3×....×an 将其划分为两个部分 左边连乘和右边连乘
左边矩阵数量可以为1个,2个。。。。n-1个,右边对应为n-1个,n-2个。。。。1个
所以
应用4:一个栈(无穷大)的进栈序列为1,2,3,…,n,有多少个不同的出栈序列?
(1)这个与加括号的很相似,进栈操作相当于是左括号,而进栈操作相当于右括号。n个数的进栈和出栈次序序列构成了一个
含2n个字数的序列或是n对括号的匹配序列。
(2)可以将进出栈序列分为两个部分,先进出栈1个,2个。。。n-1个元素,再进出栈n-1,n-2。。。1个元素。
所以
应用5:在圆上选择2n个点,将这些点成对连接起来使得所得到的n条线段不相交的方法数?
我们先举几个例子,简单的可以自己画些图
假设有2n个点,从中取任意2点连线将原来的点划分为两个部分,其中各个部分的点数都为偶数,不然不合法,必定有连线相交,设一部分中的点数为2i,则另一部分的点数为2n-2-2i。i可以取0至2n-2。
则
令h(n)=f(2n)便得到了标准的Catalan递推式。
参考文献
(1)http://www.cnblogs.com/ShaneZ...
(2)https://baike.baidu.com/item/...
(3)http://blog.csdn.net/sinat_26...
(4)https://www.cnblogs.com/dztgc...
(5)https://blog.csdn.net/acdream...
(6)http://blog.sina.com.cn/s/blo...