给定N个节点求组成二叉搜索树个数——从一道算法题探讨神奇的Catalan数

来源:互联网 发布:阿里云icon图标库 编辑:程序博客网 时间:2024/06/06 01:00

Catalan数,中文卡特兰数又称卡塔兰数,是组合数学中一个常出现在各种计数问题中的数列。一旦入坑,你会发现这个数列相当有意思,能够应用于很多看起来特别复杂的计算场景,当然,并能将之迎刃而解。

wikipedia定义:卡塔兰数是组合数学中一个常在各种计数问题中出现的数列。以比利时的数学家欧仁·查理·卡特兰(1814–1894)命名。历史上,清代数学家明安图(1692年-1763年)在其《割圜密率捷法》最早用到“卡塔兰数”,远远早于卡塔兰。有中国学者建议将此数命名为“明安图数”或“明安图-卡塔兰数”。
一般通项An=1n+1Cn2n=Cn2nCn12n

明安图《割圜密率捷法》卷三 “卡塔兰数”书影

我的“入坑”则归功于几天前在搜狐的实习生线上笔试上做到的一道题:key值分别为1,2,3,4,5,6的6个节点能够组成多少中不同的二叉搜索树(BST)。试后,我在网上查到了很多对catalan数的讨论,发现套用它的公式可以解决好多问题,甚至有不少问题都是互联网笔试中老生常谈的:

  1. n对括号 有多少种组合
  2. 矩阵链乘,依据乘法结合律,不改变其顺序,只用括号表示成对的乘积,有几种括号化的方案
  3. n个元素入栈 有多少种出栈顺序
  4. 凸多边形通过互不相交的对角线划分,求划分方案数
  5. 在圆上选择2n个点,将这些点连接起来,使得所得到的n条线段不相交的方法数
  6. 2n边的凸多边形,连接对角线 可以分出三角形的个数
  7. n × n格点中不越过对角线的单调路径的个数(上班路线选择问题)
  8. 给定n个节点组成二叉搜索树个数(或组成的二叉树形态数)
  9. 2n个高矮不同的人 站成两排 保证后排对应的人比前排高 每排从左到 右越来越高 有多少种排列方式
  10. 《编程之美》4.3中的买票找零问题:2n个人排队买票,其中n个人持50元,n个人持100元。每张票50元,且一人只买一张票。初始时售票处没有零钱找零。请问这2n个人一共有多少种排队顺序,不至于使售票处找不开钱
  11. (腾讯笔试)在图书馆一共6个人在排队,3个还《面试宝典》一书,3个在借《面试宝典》一书,图书馆此时没有了面试宝典了,求他们排队的总数?
  12. (阿里笔试)说16个人按顺序去买烧饼,其中8个人每人身上只有一张5块钱,另外8个人每人身上只有一张10块钱。 烧饼5块一个,开始时烧饼店老板身上没有钱。 16个顾客互相不通气,每人只买一个。 问这16个人共有多少种排列方法能避免找不开钱的情况出现。

这类问题恐怕每道题单拿出来都是一道令人头疼的算法编程题,仔细观察不难发现这些问题都是有一些共性的,比如都是求方案的个数,而且很多问题的应用场景都是一样的,只不过在形式在做了变形,如:(1)和(2)是一类,(4)和(5)是一类,(9)(10)(11)(12)是一类。当然,在本质上,以上所有问题均能抽象为一种问题:一种通解符合卡特兰数列的问题。(实际上,根据求解思路,我把它归纳为两类同构问题,这两种思路都能推出卡特兰数列,后面会讨论到)

内容提要

本文将首先讨论序列类场景的经典例题单调路径问题及解法,并给出几种常见的同构问题,然后以N节点二叉树问题为切入点,介绍该类问题的一般解法及思考方式,最后给出卡特兰数的一般性定义,总结卡特兰数的数学思想。文末将附上相关参考文献的链接

  • 内容提要
  • I卡特兰数性质
  • II序列类场景以NN棋盘单调路径问题为例
    • 问题描述
    • 分析
    • 同构问题
      • 出栈入栈问题
      • 矩阵链乘组括号问题
      • 买票找零或借书问题
      • 照相排队问题阿里腾讯笔试题
  • III一般性场景由递推公式求解卡特兰数
    • 问题描述
    • 分析
  • 总结

I.卡特兰数性质

II.序列类场景:以N*N棋盘单调路径问题为例

在比较了大部分常见的题型后,我发现很多例题所描述的问题都可以抽象为寻找符合若干条件的0-1序列的数量(具体有哪些条件后面我会讲到),所以我将这一类问题归纳为“序列类”问题。前面提到我一共总结了两大类,这并不意味这本类问题跟另一类是并列地位——实际上,本类问题只是对一类具体场景的概括(specific),而另一类是一般性解法(general)。
为什么要单独把它提出来讲呢?因为这类问题的解法很巧妙,没有构造递推,直接得出通项公式,而且,也确实涵盖了大部分笔试/编程题考点。很多人把这种解法称为“折现法”(《编程之美》中貌似叫“反射法”)。

问题描述

求在N*N个格点中不越过对角线的单调路径的个数,借用维基上的一张图:
这里写图片描述
左下角(0,0)点为起点,右上角(N,N)为终点。

分析

如果去掉“不能越过对角线”这个要求,我们能够很容易的算出,单调路径数为Cn2n,对于上图情形即是C48。我们用X代表“向右走一格”,Y代表“向上一格”,则每条路径可由字符串String来表示,String满足:

  1. String[i]=[X|Y]
  2. String.length=2n
  3. X与Y数量相等,均为n。
  4. 所有的前缀字串(首项为String[0]的子串)皆满足X的个数大于等于Y的个数

满足(1)(2)(3)项的String的数量我们已经计算出为Cn2n个,现考虑计算该集合下不满足(4)的情形的数量,然后减去该种情况,得到最终结果。

现从头遍历一个不满足(4)的String,记为BadStr,当遍历到第2m+1位上时有m+1个Y和m个X(容易证明一定存在这样的情况),则后面剩下的部分中必有n-m个X和n-m-1个Y。
将第2m+2位及其以后的部分做以下变换:X变成Y、Y变成X,则该部分的X现在有n-m-1个,Y有n-m个,变换后字符串记为cBadStr中共有n+1个Y和n-1个X的二进制数。注意到,对于每个BadStr,均一一对应与一个这样的cBadStr,因此NumOf(BadStrs) = NumOf(cBadStrs)=Cn12n
因此满足(1)~(4)的String数量为Cn2nCn12n

这个结果就是传说中的Catalan数


同构问题

出栈入栈问题

问题描述:对于一个无限大的栈,一共n个元素,请问有几种合法的入栈出栈形式。

分析:令1表示进栈,0表示出栈,则可转化为求一个2n位、含n个1、n个0的二进制数,满足从左往右扫描到任意一位时,经过的0数不多于1数。则结果我们可以用An表示


矩阵链乘(组括号)问题

问题描述:P=A1×A2×A3×……×An,依据乘法结合律,不改变其顺序,只用括号表示成对的乘积,试问有几种括号化的方案?

分析:将问题转化一下,就是从左到右扫描,无论扫描到任何位置,左括号数一定要大于或者等于右括号数


买票找零(或借书)问题

问题描述:16个人按顺序去买票,票价为50元,其中8个人每人身上只有一张50块钱,另外8个人每人身上只有一张100块钱。求要在售票员没有初始金钱的情况下顺利购票的排队方案

分析:带50块钱的排前面的个数总是要大于带100块钱的人的个数,即C(16,8)-C(16,7)


照相排队问题(阿里、腾讯笔试题)

问题描述:12个高矮不同的人,排成两排,每排必须是从矮到高排列,而且第二排比对应的第一排的人高,问排列方式有多少种?在一个2*n的格子中填入1到2n这些数值使得每个格子内的数值都比其右边和下边的所有数值都小的情况数?

分析:这类问题稍微比以上的问题难理解点,它的Catalan数列“隐藏”的稍微深一些,需要我们做一些分析。
我们先把这12个人从低到高排列,维护一个12位的01序列,从左到右分别对应这12个由低到高排好序的人,用0表示对应的人在第一排,用1表示对应的人在第二排,其中0出现的个数和1出现的个数相等。

比如000000111111就对应着

第一排 0 1 2 3 4 5 第二排 6 7 8 9 10 11

010101010101就对应着

第一排 0 2 4 6 8 10 第二排 1 3 5 7 9 11

很容易证明,通过这种方式得到的两排人,每排都是从矮到高排列的。那么接下来的问题就是保证第二排比第一排对应的人高。
问题转换为,这样的满足条件的01序列有多少个。
观察每一个出现的1,在这个1前面,至少要有1个0,如果1前面还有1,那么,1的个数一定应小于0,这样就回到了上面的问题中,求01序列,满足0的数量总是大于等于1。
扩展思考:如果问的不是排队的可能方案数呢?如果让你打印所有排队方案呢?回溯法能够很好的解决这类问题。
我在文末进行了更新,给出了代码求解方案。

III.一般性场景:由递推公式求解卡特兰数

该部分我会给出题目中的BST问题的解法(如果你是通过搜索题目中的二叉树关键字进来的,抱歉,现在才让你看到你想看的,哈哈),然后试图通过这类解法“感悟”卡特兰数的基本数学思想,最后我会给出自己的理解,希望能给你也带来启发。

问题描述

求N个节点构成的不同构的二叉树的个数/求N个大小不同的节点组成的二叉搜索树的不同形态数。
借用用wiki的图:
这里写图片描述

分析

  1. 先考虑只有一个节点的情形,设此时的形态由f(1)中,显然,f(1)=1;
  2. 如果有两个节点呢?现固定一个节点,那么另个节点会有左右子树两种分布情况,故有f(2)=f(1)+f(1);
  3. 再讨论三个节点的情形,仍然固定一个节点,即根节点,然后此时还剩两个节点,那么左右子树的总节点分布情况为(2,0),(1,1)和(0,2),f(3)=f(2)*f(0)+f(1)*f(1)+f(0)*f(2)。f(0)表示什么也没有,不会增加额外的情况,故取值为1;
  4. 那么对于n个节点呢?同样,固定一个节点,那么左右子树的分布情况为(n-1,0),(n-2,1),(n-3,2)……(1,n-2),(0,n-1),故f(n)=f(n-1)f(0)+f(n-2)f(1)+……+f(1)f(n-2)+f(0)f(n-1)

当得出这个公式的时候,相信大家以及明白接下来怎么做了吧。交给程序去递归就好啦。这里就不再多说。而这个公式也正是卡特兰数组的递推公式。


总结

卡特兰数的递推公式是:
C(0)=1;C(1)=1;
C(n)=C(0)*C(n-1) + C(1)*C(n-2) + …… + C(n-1)C(0);
其实序列类场景也是可以用这种一般性方法求解的,大家有没有发现,无论是上面的排队问题,多边形分割问题,还是路径规划问题,它们都有一个共同点,就是初始状态一定是确定的,也就说,序列第一项是固定的,那么剩下的n-1项就可以用分治的思路分割成两个子序列去解决就可以了。抽象点概括下,就是对于问题A,规模为n,要解决这个问题,可以用分治的思想,首先固定其中某一个元素,将剩下的n-1个元素拆分成两个小问题,这两个小问题的规模分别是(0,n-1) (1,n-2) (2,n-3) … (n-1,0)。
卡特兰数表现了一种符合乘法原理事件的本性,某种程度上,反映了我们思考问题的方式,故而能够在许多场合得到应用。
卡特兰数的递推公式可以表示为:

An+1=ni=0aAiAni

卡特兰数的通项是

An=1n+1Cn2n=Cn2nCn12n

至于如何由递推公式,推出通项的,一种思路就是我在第二部分中讲到的“折线法”,但是是通过构造具体问题推导出来的,很巧妙,看完之后我也是回味了半天。但是如果抛开这些问题,直接给你个递推公式,然后让你求通项公式,怎么下手去做呢?

有没有纯数学角度的推算方法呢?
折线法和递推求法的本质联系在哪呢?

这里留给读者,同样也是留给我的一个开发性问题。如果谁要比较好的证明思路,欢迎私信给我哈。
我的邮箱是:guojunyan93@qq.com


最后,给大家推荐一个奇妙的网站
The On-Line Encyclopedia of Integer Sequences,网址是oeis.org,对数列有兴趣的同学欢迎戳进去感受数学之美,


更新下,博主五岳在他的文章《从《编程之美》买票找零问题说起,娓娓道来卡特兰数——兼爬坑指南》讲到一种通过构造母函数来推导卡特兰通项的方法,有兴趣的同学可以去看下。


更新2017-5-19:

今天在牛客上做到一个题,用到了卡特兰数的知识。
那道题我会单独写一篇博客,这里把其中一个子问题抽象出来,给出代码方案:
给了长度为2n的顺序数列,先将数列分成两排,要求第一排的每一列小于等于对应的第二排的数字,每排顺序排列,打印出所有排序方案。

这里我用回溯方法解决,对所有方案进行深度优先遍历,如果‘0的数量大于1’(参加上文排队问题),则返回上层。
这里我构造了两个空数列:firstLine和secondLine,从头到尾遍历长度为2n的原数组arr,每次都有两个选择方案:将arr[i]放入到firstLine或secondLine。怎么放呢?没关系,我们先进行遍历,按顺序来,如果放入后firstLine的长度大于等于secondLine,则进入下层循环。另外,为了保证回溯,一定要记得及时清理状态,在每次循环后将两个数组的状态返回为上一层的样子。

代码如下:

function catalanSort(arr,firstLine,secondLine,i){    var n=arr.length/2;    if(firstLine.length==n) {        console.log(firstLine);    }    else {        for (var j = 0; j < 2; j++) {            if (j == 0) {                firstLine.push(arr[i]);            } else {                secondLine.push(arr[i]);            }            if (firstLine.length >= secondLine.length) {                catalanSort(arr,firstLine,secondLine,i+1);            }            if(j==0){                firstLine.pop();            }else{                secondLine.pop();            }        }    }}

调用方式就是:

catalanSort(arr,[],[],0)
1 0
原创粉丝点击