卡特兰数算法题整理

最近我做了很多卡特兰数相关的算法题,阅读了一些网络上的文章(本文最后有我找到的网文链接)。有的文章过于冗长,太纠结于公式的推导;有的文章又太简单,只是罗列了相关的题目却并无解题分析;而且这些文章或多或少都有一些错误。所以我写了本文,目的是简明的分析卡特兰数题目的思路和解法,但又尽量不使本文变成沉闷的数学公式推导文章。

1. 卡特兰数

卡特兰数的具体定义,可以参考wiki https://zh.wikipedia.org/zh-hans/卡塔兰数。本文只强调它的一个最重要的递推公式。
假设C(n)为自然数n对应的卡特兰数。若C(0)=1,C(1)=1,则有递推式:
C(n)= C(0)*C(n-1) + C(1)*C(n-2) + ... + C(n-1)C(0) (n>=2)
例如:C(3) = C(0)*C(2) + C(1)*C(1) + C(2)*C(0) = 5
这个递推式非常重要,下文的每一题都几乎应用了这个公式。而且也正是这个递推式让我们可以用动态规划或者卡特兰数的通项公式求解卡特兰数问题。

2. 算法题汇总

1)假设一个二叉树有n个节点,求所有异构二叉树数量。
这道题来自 LeetCode 第96题 Unique Binary Search Trees。https://blog.csdn.net/tassardge/article/details/82916316 

选取任意一个结点为根,就把二叉树切成左右两个子树,以这个结点为根的可行二叉树数量就是左右子树可行二叉树数量的乘积。遍历所有的节点并把所有可行结果累加,就得到了总数。写成表达式如下:

C(n)= C(0)*C(n-1) + C(1)*C(n-2) + ... + C(n-1)C(0) (n>=2)

很明显这正是卡特兰数的递推式。动态规划的解如下:

class Solution {
public:
    int numTrees(int n) {
        if (n == 0)
            return 0;

        vector<int> res;
        res.push_back(1);
        res.push_back(1);

        for (int i = 1; i<n; ++i) {
            res.push_back(0);
            for (int j = 0; j<=i; ++j)
                res[i+1] += res[i-j]*res[j];
        }

        return res[n];
    }
};

以下各题的解法都与本题相同,所以下文不再列举代码。

2)n对左右括号组成一个字符串。求所有满足以下条件的字符串数:要求遇到左括号则入栈,遇到右括号则出栈;处理完后,栈为空。
我们把n对左右括号看作有2n+1个节点的二叉树。并且使所有节点的左子节点对应左括号,右子节点对应右括号。这样,除了根节点外的所有节点都与左或者右括号对应。对所有的异构二叉树做遍历就得到了所有满足条件的字符串。所以,这道题的本质是求异构二叉树的数量。

例如,假设我们求3对左右括号有多少满足条件的字符串组合,则可求7个节点的异构二叉树,并作遍历即可。如下图。

3)2n个人排队买票,其中n个人持50元,n个人持100元。每张票50元,且一人只买一张票。初始时售票处没有零钱找零。请问这2n个人一共有多少种排队顺序,不至于使售票处找不开钱?
如果把持50元的人看作左括号,持100元的人看作右括号,那么这道题其实就是问题2)的变体。所以,这道题的本质是求异构二叉树的数量。

4)n个数连乘,求所有可能的乘法顺序。
我们把二叉树的每个叶节点看作是乘数,那么遍历这颗二叉树就得到了一种可能的乘法顺序。所有可能的乘法顺序就是求有2n+1个节点的异构二叉树的总数。所以,这道题的本质是求异构二叉树的数量。

例如,我们有4个数a b c d连乘,那么转换成二叉树就是:

5)对于一个n*n的正方形网格,每次我们能向右或者向上移动一格,那么从左下角到右上角的所有在副对角线右下方的路径总数为多少。引用Wikipedia上的图片:

如果向右走看作左括号,向上走看作右括号,那么这道题其实就是问题2)的变体。所以,这道题的本质是求异构二叉树的数量。

6)凸n+2边形进行三角形分割数,要求只连接顶点对形成n个三角形。问有多少种不同的分法。
设边数为n+2,选定任意一条边作为第一个三角形的边,再为它选择一个顶点k,1<=k<=n。n边型被分为了一个k边形、一个三角形和一个n-k边形。此时的划分数为C(k)*C(n-k)。所以对所有的顶点遍历后,有卡特兰递推式:
C(n)= C(0)*C(n-1) + C(1)*C(n-2) + ... + C(n-1)C(0) (n>=2)

7)求n个数的入栈出栈的排列总数。例如1,2,3的入栈出栈排序有123,132,213,231和321五种。
假设有2n+1个节点的二叉树的每个非叶节点代表一个数字,那么对所有异构二叉树做遍历,就得到了结果。所以,这道题仍是异构二叉树问题。

8)求有2n个数字的集合{1,2,...,2n}的不交叉划分的数目。
这里解释一下不交叉划分。假设将集合{a,b,c,d}划分为两个子集{a,b}和{c,d},组成了两个区间[a,b]和[c,d]。如果这两个区间不重合,那么以下四种情况是不交叉的:a<c<d<b,a<b<c<d,c<a<b<d与c<d<a<b,就是说两个区间可以包含或者相离,那么此时我们称集合{a,b}和{c,d}是不交叉的。
对于集合{1,2,...,2n},将其元素两两分为一子集,共n个;若对于一个划分,任意两个子集都是不交叉的,那么我们称此时的这个划分为一个不交叉划分。如果此时我们将每个子集中较小的数用左括号代替,较大的用右括号代替,那么带入原来的1至2n的序列中就变成了合法括号问题,就是问题2)。

9)求n层的阶梯切割为n个矩形的切法数。如下图所示(图片来自wiki):

我们先绘制如下的一张图片,即n为5的时候的阶梯:

我们注意到每个切割出来的矩形都必需包括一块标示为*的小正方形,那么我们此时枚举每个*与#标示的两角作为矩形,剩下的两个小阶梯就是我们的两个更小的子问题了,于是我们的C(5) = C(0)*(4) + C(1)*C(3) + C(2)*C(2) + C(1)*C(3) + C(0)*C(4)

10)向一个两行n列的方阵中填入数字1到2n,求使每个方格内的数值都比其右边和下边的方格内的数值小的排列数。
该题的另一种问法:12个高矮不同的人,排成两排,每排必须是从矮到高排列,而且第二排比对应的第一排的人高,问排列方式有多少种。

假设我们构造一个有2n+1个节点的二叉查找树(BST),除了根节点以外的所有节点都填上从1到2n的数字。后序遍历该二叉树便得到了一种排列方法。所以,这道题的本质仍然是求异构二叉树。

3. 总结:

1)以上所有题目实际上都是先得到卡特兰数的递推公式,然后求解。
2)以上大部分题目很多都是二叉树的变体,可见二叉树的基础有多重要。
3)以下是两篇本文主要引用的文章的链接:

https://blog.csdn.net/Hackbuteer1/article/details/7450250

http://www.cnblogs.com/wuyuegb2312/p/3016878.html

猜你喜欢

转载自blog.csdn.net/tassardge/article/details/82926832