卡特兰数( Catalan 数)指的是一个特殊的数列,可以是一个计算方案总数的一个比较好用的工具。而卡特兰数解决的问题可以概括为:对于两种操作,操作一和二都分别做 n 次,总共 2n 次,而且要求操作途中操作一已操作次数必须时刻大于等于操作二的已操作次数,求总共有几种的操作方案。说起来比较绕,但是我们可以根据几个题目来理清卡特兰数的定义及公式推导。
dp递推式
洛谷题目链接:P1044 NOIP 2003 普及组 栈 - 洛谷
这是一个卡特兰数的一个经典题目,虽然一眼看上去看不出有很大的联系。首先不去想卡特兰数这件事,其实用动态规划 DP 就能够解决这道题。可以发现如果对于 n 长度序列 1-n ,我们可以去枚举分割点来计数。比如我先让前面 1,i-1 个数都完成入栈出栈,再对后续 i,n 这些数字来进行入栈出栈,则根据乘法计数原理,在 i 这个点的方案数就应该是前面 1,i-1 和后续 i,n 各自方案数的乘积。枚举 1-n 每个点作为分割的方案数加起来就是答案了。
所以这其实就是 dp 由小算大的过程,公式如下。
据此很容易得到对应的代码。
cpp
#include <iostream>
using namespace std;
long long f[30];//dp数组,f[i]代表总共i个数有多少种方案数
int main(){
long long n;
cin>>n;
f[0]=f[1]=1;//规定f[0]就是1,也符合题意,什么都没有也应该算一种方案,而f[1]显而易见是1
for(int i=2;i<=n;++i){
for(int j=1;j<=i;++j){
f[i]+=f[j-1]*f[i-j];
}
}
cout<<f[n];
return 0;
}
如果按照动态规划 dp 的方法来思考,这题本身也并不难。但可以发现,栈当然需要栈的内部有元素才能出栈啊!所以说,入栈次数必须是时刻大于等于出栈次数 的,而且可以发现每个数字都会进行一次入栈和一次出栈,也就是说入栈总次数和出栈总次数是相同的都是 n 次 。入栈和出栈分别对应着两种不同操作。可以发现这正好就符合卡特兰数的定义。
所以这正好就是递推式计算卡特兰数的方法及公式,时间复杂度是 O(n\^2) 。这似乎比较慢,但还是一个相对好写不会出错的写法。
通项公式及线性递推式
洛谷题目链接:P1754 球迷购票问题 - 洛谷
在这个题目就更加的明显是卡特兰数了,很容易理解到 50 和 100 对应着两种操作,而由于需要找零,50 的已操作数必须大于或者等于 100 。这里当然也可以用 dp 进行计算,但我们也可以通过卡特兰数的通项公式或者线性递推公式 来算得最后的答案。我们可以通过折线对称法来推导一下这个公式。
我们可以想象一个坐标系,使用 50 操作对应在 x 轴 +1 ,使用 100 操作对应在 y 轴 +1。而由于他们的操作分别为 n 次,则我们从 (0,0) 点出发只要是符合条件的方案,最终都会走到 (n,n) 点处。

所以说,如果不考虑是否满足条件 x>=y ,只考虑走 2n 步能抵达 (n,n) 点的方案数,那就很显然是组合数**** 。但我们要求合法的方案数必须时刻满足 x>=y ,所以我们要算出所有不合法的方案并减去才是最终答案。
而为了满足 x>=y 这个条件,可以发现我们就始终不能越过 y=x 这条直线,一旦触碰到 y=x+1 这条直线,就肯定是一种不合法方案。

现在假设有任意一条路线是已经碰到了 y=x+1 这条线,也就是说,无论在哪里碰到的,只要在到达终点之前触碰到 y=x+1 ,假设触碰点为 P 点,那这就是一条不合法路径。而因为最终我们要去到的终点就是 (n,n) ,但为了方便计算,我们将从 P 到 (n,n) 的这个路线相对于 y=x+1 做一个对称,即 (n,n) 的对称点为 (n-1,n+1) 。

做完对称点可以发现,任意一条路线方案只要经过了 y=x+1 这条线,且它的目标终点是 (n,n) ,那么它肯定也可以到达对应的对称点 (n-1,n+1) 。换句话说,任意一条不合法路径都是必须经过 y=x+1 且最后可以抵达 (n,n) 的点,都对应着一条到达对称点 (n-1,n+1) 的路径 。所以,从 (0,0) 到 (n-1,n+1) 的所有路线方案也就是所有不合法的方案 。利用对称点计算的好处就是如果直接算到达 (n,n) 的不合法路径,我们需要知道到底有没有跨越 y=x+1 这条线 ,而对称点天生就在 y=x+1 这条线上方,要到达则必须经过 y=x+1 ,所以每一条都必然是不合法方案。最后算得的不合法路径总数也就是组合数 。
则卡特兰数的通项公式为
但有时候,如果题目没有让我们取模计算,那阶乘会大概率爆掉。所以通过通项公式可以得到相邻两项的线性递推式。
知道了这两个式子之后,回看这道洛谷的 P1754 当然最好是使用线性递推式进行计算 ,时间复杂度也就 O(n) ,相比于 dp 递推式快了不少。
cpp
#include <bits/stdc++.h>
using namespace std;
#define int long long
int c[25];
void solve(){
int n;
cin>>n;
c[0]=1;
for(int i=1;i<=n;++i){
c[i]=c[i-1]*(4*i-2)/(i+1);
}
cout<<c[n]<<'\n';
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);cout.tie(nullptr);
int t=1;
//cin>>t;
while(t--) solve();
return 0;
}
总结
由刚刚的推导,实际上卡特兰数我们就有以下三种计算方法。
1. dp 递推:
2.通项公式:
3.线性递推式:
具体用哪个更好,还是视题目而定。但其实卡特兰数难的点并非是计算过程,更多是能不能看出这道题要用卡特兰数。但其实一般题目只要感觉到有两种操作且有一种操作必须时刻比另一个多的时候一般就是要考虑卡特兰数了。而且卡特兰数主要是用于计算方案数的,如果列举前几个方案数发现是大概是 1,1,2,5,14,42 这样的呈现类似卡特兰数的数列也可以考虑使用。