本文的读者应该具有程序设计的基础,且已经编写过一些递归题目的代码。
(一) 前言
递归是算法领域最重要的基础算法之一,在后续的动态规划算法、搜索算法中也会经常用到递归思想。
递归算法常用于以下三种场景:
(1)递归定义的数学函数(阶乘、阿克曼函数等)
(2)具有递归特性的数据结构(树,二叉树,堆等)
(3)其他可用递归求解的问题
通常用F函数表示递归,例如F(n)表示一个数据规模为n的递归函数。递归算法的思想就是思考如何用问题规模小于n的递归函数,如F(n-1)、F(n/2)等,设计算法求得F(n)。
和学习其他算法一样,递归思想的培养既要一定量的训练,也要思考。学习者应该先学习并掌握一些和递归密切相关的方法(基础性递归问题,以及递归实现指数枚举,组合、排列等)和数学概念(斐波那契,卡特兰数),再通过训练、思考,总结来逐步掌握递归算法和思想。本文仅从递归的角度,对卡特兰数及其相关问题进行一些基础性介绍。
(二) 什么是卡特兰数
卡特兰数(英语:Catalan number),又称卡塔兰数、明安图数,是组合数学中一种常出现于各种计数问题中的数列。以比利时的数学家欧仁·查理·卡特兰的名字来命名。卡特兰数前几项为(从第0项开始):1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862, 16796, 58786, 208012, 742900, 2674440, 9694845, 35357670, 129644790, 477638700, 1767263190, 6564120420, 24466267020, 91482563640, 343059613650, 1289904147324, 4861946401452, ...(编程时尽量用long long类型)
(三) 卡特兰数对应的递归问题
在数据结构与算法领域,相当多的问题与卡特兰数相关。典型的有:n元素的出栈序列问题、二叉树的形态问题、凸多边形的三角划分问题(有代码)、括号匹配方案问题等。
(1)n元素出栈序列问题
题目:1,2,3......n个元素按次序依次进栈,请问共有多少种出栈序列方案。例如n=3时,答案是5,出栈序列分别是123,132,213,231,321。
解法:用递归思想,如何将F(n)问题变小?思考最后一个出栈元素,观察发现其值可能是1....n任意一个,假定最后一个出栈元素是i,那么1,2....i-1在i入栈之前一定已经出栈(不然i不会是最后一个),i入栈后,i+1,i+2......n依次入栈。注意:i前面入栈元素的出栈序列与i之后入栈元素的出栈序列没有任何关联和影响,因此:
以i为最后一个元素的出栈序列方案数=前i-1个的方案数 * 后n-i个的方案数。
i可以是1....n任何一个,由此得到计算n个元素出栈序列的递归计算公式(卡特兰公式):
F(n)=F(0)∗F(n−1)+F(1)∗F(n−2)+F(2)∗F(n−3)+...+F(n−1)∗F(0)
(2)n个点二叉树的形态问题
题目:n个结点的二叉树,有多少种不同的形态?
解法:二叉树分为三部分,树根,左子树,右子树,其中左右子树可以为空。设n个点二叉树形态为F(n),那么拿出一个点作为树根,此时有n-1个点可以分给左右子树,分配方案共有n种:分别是左0右n-1,左1右n-2.........左n-1右0。由于左右子树形态并无关联和影响,因此左子树分配i个结点,右子树分配n-1-i个结点的方案数为F(i)*F(n-1-i)。
总的方案数为: F(n)=F(0)∗F(n−1)+F(1)∗F(n−2)+F(2)∗F(n−3)+...+F(n−1)∗F(0)
OJ题目:11082 完全二叉树的种类,n个叶子结点的完全二叉树,其形态有多少种?(完全二叉树意味着每个分支结点都有2个儿子结点)
解法:n个叶子被分配给左右子树,注意子树不能有0个叶子。因为分配方案为左1右n-1,左2右n-2.........
总方案数为: F(n)=F(1)∗F(n−1)+F(2)∗F(n−2)+F(3)∗F(n−3)+...+F(n−1)∗F(1)
这个公式和上面略有不同,思考把F()函数每一项都减一,实际上这个公式得到的结果是标准卡特兰的F(n-1)算式。
(3)凸多边形的三角划分问题-10343 划分凸多边形
题目:一个正凸N边形,用N-3条互不相交的对角线将正N边形分成N-2个三角形的划分方案数。
解法:给结点进行编号1至N,为了降低问题规模,先将1号和n号与其他点连接(不能连1和N),共有N-2种连接方案,假定1号n号和结点i进行连线,构成三角形(1,n,i)将凸N边形划分成两部分,一边是一个凸i边形(结点1....i),另一个是一个凸N-i+1边形(结点i.....n),这两部分的划分是独立的。因此当结点1和n与i连线时,划分方案数=F(i)*F(n-i+1)。
总方案数为: F(n)=F(2)∗F(n−1)+F(3)∗F(n−2)+F(4)∗F(n−3)+...+F(n−1)∗F(2)
这里计算时F(2)=F(3)=1,三角形就一种划分方案,这个F(3)其实和和标准卡特兰的F(1)一样,F(4)=2,四边形有两种划分方案。因此此题目F(n)相当于标准卡特兰的F(n-2)。
计算卡特兰数时有两个点需要注意,第一,卡特兰上升速度极快,因此尽量用longlong类型存储,第二,递归思想可以,但递归算法效率较差,用递推替换递归,降低复杂度。
#include <iostream>
using namespace std;
int main()
{
int i,j,n;
long long f[25]={0};
cin>>n;
if(n<3)//不满足多边形划分条件
{
cout<<"No answer";
return 0;
}
f[0]=f[1]=1;//卡特兰前两项
for(i=2;i<=n-2;i++)
{
for(j=0;j<i;j++)
f[i]+=f[j]*f[i-j-1];
}
cout<<f[n-2];//凸多边形问题需要-2
return 0;
}
(4)括号匹配问题
题目:由 N个“(”和 N 个“)”组成的字符串,要求左括号和右括号是匹配的。例如“)(())(”和“())(()”都是不匹配的括号串。当N=3时,有以下5种匹配的括号串:
解法:只关注第一个字符左括号,其匹配对象位置只能是偶数项位置(自行思考),那么这个右括号可能放在最后一个字符(2*N位置),倒数第二个偶数位(2*(N-1)),依次2*(N-2).......,2。和前面几个例子类似,第一个括号和其匹配括号会将序列分割成两段,当第一个左括号与2*i位置右括号匹配时,序列左段长度为2*(i-1),右段长度为2*(n-i),两段分割方案相互独立。
总方案数为: F(2n)=F(0)∗F(2n−2)+F(2)∗F(2n−4)+F(4)∗F(2n−6)+...+F(2n−2)
最后一项为第一个左括号和最后一个右括号匹配,问题转换为F(2n−2),当然,上述方程实际上就是标注的卡特兰数方案。
括号匹配问题的几个拓展:
11085 买票找零 (50元看成左括号,100元看成右括号)
10344 矩阵连乘积的加括号方式数 (N个矩阵对应N对括号)
17083 多重幂计数问题 (和上题一样)
教材课后习题 2-10 标准二维表问题
上图为转载,原图地址为卡特兰数(Catalan number)(一) - 知乎,有兴趣同学可看,里面还有几个例子。
(四) 结语
少说多做,少讲多练~~!