卡特兰数
卡特兰数
简介
卡特兰数(英语:Catalan number),又称卡塔兰数、明安图数,是组合数学中一种常出现于各种计数问题中的数列。以比利时的数学家欧仁·查理·卡特兰的名字来命名。1730年左右被蒙古族数学家明安图使用于对三角函数幂级数的推导而首次发现,1774年被发表在《割圜密率捷法》。
题目特征
给定 \(n\) 个 \(0\) 和 \(n\) 个 \(1\),它们将按照某种顺序排成长度为 \(2n\) 的序列,求它们能排列成的所有序列中,能够满足任意前缀序列中 \(0\) 的个数都不少于 \(1\) 的个数的序列有多少个。这个求解的数字就被称作卡特兰数。
公式
记这个数列为 \(C_n\)。
它还满足以下递推关系:
证明
首先我们需要将上述问题转换成一个等价的问题:在一个二维平面内,从 (0, 0) 出发到达 (n, n) ,每次可以向上或者向右走一格,0 代表向右走一格,1 代表向上走一格,则每条路径都会代表一个 01 序列。
我们可以先计算出所有的路径条数应该为 \(C_{2n}^{n}\) 条。这可以理解为:你一共需要走 \(2n\) 步,其中选择 \(n\) 步向上走,剩下 \(n\) 步自然是向右走。
根据题意,满足任意前缀中 0 的个数不少于 1 个数序列对应的路径则应该在下图的右下侧。

符合要求的路径必须严格在上图中红色线的下面(不可以碰到图中的红线,可以碰到绿线)。则我们考虑任意一条不合法路径,例如下图中的橙线就是不合法的:

经过上图所示的翻折后,原本的终点(6,6)变成了(5,7)。容易看出,任何不合法的路线均可以进行上图的翻折且终点为(n-1,n+1)。而且,从(0,0)到(n-1,n+1)的任意一条直线一定会过红线,且与一条非法的路线一一对应。这样一来,非法路线条数就是从(0,0)到(n-1,n+1)的总路线条数 \(C_{2n}^{n-1}\)。
因此,这个题的答案就是 \(C_{2n}^n-C_{2n}^{n-1}\).
根据组合数的公式简单推导,便可得出其也等于 \(\frac{C_{2n}^{n}}{n+1}\).
适用例题
- 01序列问题(如上描述)
- 方格路线问题(如上证明)
- 圆内连弦问题:圆上有 \(2n\) 个点,以这些点为端点连互不相交的 \(n\) 条弦,求连接的所有方法总数。(见下题七)
- 凸多边形的剖分:求凸 \(n + 2\) 边形用其 \(n − 1\) 条对角线分割为互不重叠的三角形的分法。
- 括号序列问题:有 \(n\) 个
(和 \(n\) 个),问正确匹配的括号序列有多少种组合方式? - 进出栈问题:有 \(n\) 个不同的数字依次进栈出栈,问有多少种不同的进出栈顺序?
- 二叉树计数问题(见下题三、四)
- 312排列 ,一个长度为 \(n\) 的排列 \(a\),只要满足 \(i<j<k\),且 \(a_j<a_k<a_i\),这个排列就称为312排列。求 \(n\) 的全排列中不是312排列得排列个数。(其实312排列就是所有不能用 \(n\) 个数进出栈表示出来的排列)
- 有 \(2n\) 个人排成一行进入剧场,入场费5元。其中只有 \(n\) 个人有一张5元钞票,另外 \(n\) 人只有10元钞票,剧院无准备钞票以供找零。问有多少中方法使得只要有10元的人买票,售票处就有5元的钞票找零?(将持5元者到达视作将5元入栈,持10元者到达视作使栈中某5元出栈)
经验总结
看到输入样例为 3 且输出样例为 5 时一定要敏锐地意识到此题可能是在考察卡特兰数。
判断一个题是否是卡特兰数一般有两种方法:
- 符合经典题型:有 \(n\) 个 \(1\) 和 \(m\) 个 \(0\) 组成字符串,且在任意的前 k 个字符中,1 的个数不能少于 0 的个数。(\(n\) 和 \(m\) 不同时,这属于卡特兰数的变形)
- 满足递推关系:\(C_n=\sum\limits_{i=1}^nC_{i-1}\cdot C_{n-i}\)
例题及代码展示
一、889. 满足条件的01序列 - AcWing题库
题目
给定 \(n\) 个 \(0\) 和 \(n\) 个 \(1\),它们将按照某种顺序排成长度为 \(2n\) 的序列,求它们能排列成的所有序列中,能够满足任意前缀序列中 \(0\) 的个数都不少于 \(1\) 的个数的序列有多少个。
输出答案对 \(10^9+7\) 取模。
输入格式
共一行,包含整数 \(n\)。
输出格式
共一行,包含一个整数,表示答案。
数据范围
\(1 \le n\le 10^5\)
分析
此题只求一个数的组合数,直接用定义求即可。
代码
#include <iostream>
using namespace std;
typedef long long LL;
const int mod = 1e9 + 7;
int qmi(int a) {
int res = 1, k = mod - 2, p = mod;
while (k) {
if (k & 1)
res = (LL)res * a % p;
k >>= 1;
a = (LL)a * a % p;
}
return res;
}
int main() {
int n, res = 1;
cin >> n;
for (int i = (n << 1); i > n; i--)
res = (LL)res * i % mod;
for (int i = 2; i <= n; i++)
res = (LL)res * qmi(i) % mod;
res = (LL)res * qmi(n + 1) % mod;
cout << res;
return 0;
}
二、415. 栈 - AcWing题库,洛谷P1044 [NOIP2003 普及组] 栈
题目
栈是计算机中经典的数据结构,简单的说,栈就是限制在一端进行插入删除操作的线性表。
栈有两种最重要的操作,即 \(pop\)(从栈顶弹出一个元素)和 \(push\)(将一个元素进栈)。
栈的重要性不言自明,任何一门数据结构的课程都会介绍栈。
宁宁同学在复习栈的基本概念时,想到了一个书上没有讲过的问题,而他自己无法给出答案,所以需要你的帮忙。
宁宁考虑的是这样一个问题:一个操作数序列,从 \(1,2,\dots,n\),栈 \(A\) 的深度大于 \(n\)。
现在可以进行两种操作,
- 将一个数,从操作数序列的头端移到栈的头端(对应数据结构栈的 \(push\) 操作)。
- 将一个数,从栈的头端移到输出序列的尾端(对应数据结构栈的 \(pop\) 操作)。
使用这两种操作,由一个操作数序列就可以得到一系列的输出序列。
你的程序将对给定的 \(n\),计算并输出由操作数序列 \(1,2,\dots ,n\) 经过操作可能得到的输出序列的总数。
输入格式
输入文件只含一个整数 \(n\)。
输出格式
输出文件只有一行,即可能输出序列的总数目。
数据范围
\(1\le n\le 18\)
分析
此题同样只求一个数的组合数,由于 \(n\) 很小,但又不能直接使用公式,因为 \(36!\) 会爆 long long;又因为 \(C_{36}^{18}\) 不超过 \(10^{10}\),开 long long 用递推法求组合数即可。
此题甚至可以直接打表,下面给出两种代码。
代码
1.递推:
#include <iostream>
using namespace std;
typedef long long LL;
LL c[40][20];
int main() {
int n;
cin >> n;
for (int i = 0; i <= (n << 1); i++)
c[i][0] = 1;
for (int i = 1; i <= (n << 1); i++)
for (int j = 1; j <= min(i, n); j++) //相当于只计算杨辉三角的左半边
c[i][j] = c[i - 1][j] + c[i - 1][j - 1];
LL res = c[n + n][n];
cout << res / (n + 1);
return 0;
}
2.打表:
#include <iostream>
using namespace std;
int f[] = {0, 1, 2, 5, 14, 42, 132, 429, 1430, 4862, 16796, 58786, 208012, 742900, 2674440, 9694845, 35357670, 129644790, 477638700};
int main() {
int n;
cin >> n;
cout << f[n];
return 0;
}
三、1257. 二叉树计数 - AcWing题库
题目
由 \(n\) 个节点最多可组成多少个不同形态的二叉树?
输入格式
包含一个正整数 \(n\)。
输出格式
输出一个整数,表示不同形态的二叉树的个数。
数据范围
\(1\le n\le 5000\)
分析
设 \(f(n)\) 表示 \(n\) 个节点可以构成的不同形态的二叉树数目,选择一个节点作为根节点,其左右子树节点数目为 \(i-1\) 和 \(n-i\)( \(i\)范围是 \([1,n]\)),因此根据乘法原理,这样的二叉树形态个数为 \(f(i-1) \times f(n-i)\),根据加法原理,有:
其恰好为卡特兰数的递推关系式。
此题不要求取模,\(n\) 又很大,所以需要使用高精度,以及质因数分解和约分。
代码
#include <iostream>
#include <cstring>
#define MOD 10000
using namespace std;
const int N = 1e5 + 5;
int p[N], t;
bool s[N];
int sum[N]; //sum[i]表示第i个质数p[i]出现的次数(指数)
struct HP { //高精度
int l[4000];
int len;
HP() {
memset(l, 0, sizeof(l));
len = 0;
}
void print() {
printf("%d", l[len]);
for (int i = len - 1; i > 0; i--) {
printf("%04d", l[i]);
}
printf("\n");
}
};
HP operator*(const HP &a, const int &b) { //高精乘低精
HP c;
c.len = a.len;
int x = 0;
for (int i = 1; i <= c.len; i++) {
c.l[i] = a.l[i] * b + x;
x = c.l[i] / MOD;
c.l[i] %= MOD;
}
while (x) {
c.len++;
c.l[c.len] = x % MOD;
x /= MOD;
}
return c;
}
void prime(int n) { //筛n以内的素数
int i, j;
for (i = 2; i <= n; i++) {
if (!s[i]) p[++t] = i;
for (j = 1; p[j] <= n / i; j++) {
s[i * p[j]] = 1;
if (i % p[j] == 0) break;
}
}
}
int get(int n, int p) { //n!中p这个质数出现了几次
int res = 0;
while (n) {
res += n / p;
n /= p;
}
return res;
}
int main() {
int n;
cin >> n;
prime(n << 1);
for (int i = 1; i <= t; i++) {
int pr = p[i];
sum[i] = get(n << 1, pr) - 2 * get(n, pr); //相当于约分操作
}
//需要额外对(n+1)进行质因数分解,再约分
int temp = n + 1, cnt;
for (int i = 1; i <= t; i++) {
cnt = 0;
while (temp % p[i] == 0) {
temp /= p[i];
cnt++;
}
sum[i] -= cnt;
if (temp == 1) break;
}
HP res;
res.l[1] = 1;
res.len = 1;
for (int i = 1; i <= t; i++)
for (int j = 1; j <= sum[i]; j++)
res = res * p[i]; //做乘法
res.print();
return 0;
}
四、1645. 不同的二叉搜索树 - AcWing题库
题目
给定一个整数 \(n\),求以 \(1,2,\dots ,n\) 为节点组成的二叉搜索树有多少种?
结果对 \(10^9+7\) 取模后输出。
输入格式
共一行,包含一个整数 \(n\)。
输出格式
输出一个整数,表示对 \(10^9+7\) 取模后的结果。
数据范围
\(1\le n\le 1000\)
分析
二叉搜索树要满足:任意节点的值一定大于左子节点的,一定小于右子节点的。
设函数 \(f(n)\) 表示以 \(n\) 个不同的数为节点能组成的搜索二叉树的总数。
假设树根为 \(i\),其左子树一定包含 1~i-1 的所有这 \(i\) 个数,其右子树 i+1~n 的所有这 \(n-i\) 个数。根据乘法原理,树根为 \(i\) 时的情况数为 \(f(i-1) \times f(n-i)\)。又因树根有 \(n\) 种情况,根据加法原理,有:
此题代码与第一题代码完全一致,不过多赘述。
五、1317. 树屋阶梯 - AcWing题库,洛谷P2532 [AHOI2012] 树屋阶梯
题目
暑假期间,小龙报名了一个模拟野外生存作战训练班。
训练的第一个晚上,教官就给他们出了个难题。
由于地上露营湿气重,必须选择在高处的树屋露营。
小龙分配的树屋建立在一颗高度为 \(N+1\) 尺的大树上,正当他发愁怎么爬上去的时候,发现旁边堆满了一些空心四方钢材(如图 \(1.1\))。
经过观察和测量,这些钢材截面的宽和高大小不一,但都是 \(1\) 尺的整数倍,教官命令队员们每人选取 \(N\) 个空心钢材来搭建一个总高度为 \(N\) 尺的阶梯来进入树屋,该阶梯每一步台阶的高度为 \(1\) 尺,宽度也为 \(1\) 尺。
如果这些钢材有各种尺寸,且每种尺寸数量充足,那么小龙可以有多少种搭建方法?

注:为了避免夜里踏空,钢材空心的一面绝对不可以向上。
输入格式
一个正整数 \(N\),表示阶梯的高度。
输出格式
一个正整数,表示搭建方法的个数。
注:搭建方法个数可能很大。
数据范围
\(1\le N\le 500\)
输入样例:
3
输出样例:
5
样例解释
\(5\) 种搭建方法如图 \(1.2\) 所示:

分析
由题可知:要用 \(n\) 个钢材搭 \(n\) 级台阶,显然每级台阶都是一个钢材。
一定存在以一个钢材(第 \(i\) 个)占据了最左下角的区域(下图红色),且这个钢材可以将所有其他钢材分为上下两部分,即它上面要搭 \(n-i\) 个钢材的台阶(下图绿框),下面要搭 \(i-1\) 个钢材的阶梯(下图蓝框)。
而 \(i\) 的取值是 1~n ,故满足递推关系:\(C_n=\sum\limits_{i=1}^nC_{i-1}\cdot C_{n-i}\)。此题考查卡特兰数。

六、 洛谷P3200 [HNOI2009] 有趣的数列
题目
我们称一个长度为 \(2n\) 的数列是有趣的,当且仅当该数列满足以下三个条件:
-
它是从 \(1 \sim 2n\) 共 \(2n\) 个整数的一个排列 \(\{a_n\}_{n=1}^{2n}\);
-
所有的奇数项满足 \(a_1<a_3< \dots < a_{2n-1}\),所有的偶数项满足 \(a_2<a_4< \dots <a_{2n}\);
-
任意相邻的两项 \(a_{2i-1}\) 与 \(a_{2i}\) 满足:\(a_{2i-1}<a_{2i}\)。
对于给定的 \(n\),请求出有多少个不同的长度为 \(2n\) 的有趣的数列。
因为最后的答案可能很大,所以只要求输出答案对 \(p\) 取模。
输入格式
一行两个正整数 \(n,p\)。
输出格式
输出一行一个整数表示答案。
样例 #1
样例输入 #1
3 10
样例输出 #1
5
提示
【数据范围】 对于 \(50\%\) 的数据,\(1\le n \le 1000\); 对于 \(100\%\) 的数据,\(1\le n \le 10^6\),\(1\le p \le 10^9\)。
【样例解释】 对应的5个有趣的数列分别为(1,2,3,4,5,6),(1,2,3,5,4,6),(1,3,2,4,5,6),(1,3,2,5,4,6),(1,4,2,5,3,6)。
分析
从1到 \(2n\) 依次考察每个元素放置的位置,1 只能放在第一个位置,2 只能放在第二个位置,且任意时刻我们放置的数据中奇数项的个数必须大于等于偶数项的数量。否则,假设我们奇数项放置2个元素,偶数项放置3个元素,则不合法,如下图:

我们可以这样对应:在从1到 2n 依次考察每个元素时,如果这个数据放到奇数位置,标为0,否则标为1。则任意前缀中0的个数要大于等于1的个数。
因此此题考查卡特兰数。
由于此题 \(p\) 不固定,不可求逆元,只能使用分解质因数再约分的方法求组合数。
代码
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 2e6 + 5; //注意要大于10^5的两倍
int pr[N], cnt;
bool s[N];
int sum[N];
void prime(int n) {
s[0] = s[1] = 1;
for (int i = 2; i <= n; i++) {
if (!s[i]) pr[++cnt] = i;
for (int j = 1; pr[j] <= n / i; j++) {
s[i * pr[j]] = 1;
if (i % pr[j] == 0) break;
}
}
}
int get(int n, int p) {
int res = 0;
while (n) {
res += n / p;
n /= p;
}
return res;
}
int main() {
int n, mod;
cin >> n >> mod;
prime(n << 1);
for (int i = 1; i <= cnt; i++) {
int p = pr[i];
sum[i] = get(n << 1, p) - 2 * get(n, p);
}
int t = n + 1;
for (int i = 1; i <= cnt; i++) {
int p = pr[i];
while (t % p == 0) {
sum[i]--;
t /= p;
}
if (t == 1) break;
}
int res = 1;
for (int i = 1; i <= cnt; i++)
for (int j = 1; j <= sum[i]; j++)
res = (LL)res * pr[i] % mod;
cout << res;
return 0;
}
七、圆内连弦
题目
圆上有 \(2N\) 个不同的点,小 x 想用 N 条线段把这些点连接起来(每个点只能连一条线段), 使所有的线段都不相交,他想知道这样的连接方案有多少种?
分析
我们规定 start 为起始点,顺时针方向为正方向。
从 start 开始,将一条弦 最先遇到的那个端点标记为 +,后来遇见的另一个端点为标记为 −,就可以得到如下图。
对于标蓝的点来说,我们可以看出,在它之前,+ 号的标记是必须大于等于 - 号的标记的。
参考资料:

浙公网安备 33010602011771号