卡特兰数--zhengjun

卡特兰数简介

卡特兰数其实不是一个数,而是一个数列。

一、了解卡特兰数

卡特兰数又称卡塔兰数,它是组合数学中一个常出现在各种计数问题中出现的数列,其前几项为 :

\(1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862,\)
\(16796, 58786, 208012,...\)

关于卡特兰数公式有好多,先罗列几个,暂时不推导。

\(f(n)=\dfrac{1}{n+1}C_{2n}^n\)

\(f(n)=\sum\limits_{i=0}^{n-1}(Catalan_i\times Catalan_n-i)\)

\(f(n)=C_{2n}^n-C_{2n}^{n-1}\)

卡特兰数模型应用:

(1)二叉树的计数:

已知二叉树有 n 个结点,求能构成多少种不同的二叉树

(2)括号化问题:

一个合法的表达式由()包围,()可以嵌套和连接,如:(())()也是合法表达式,现给出n 对括号,求可以组成的合法表达式的个数

(3)划分问题:

将一个凸 n+2 多边形区域分成三角形区域的方法数

(4)出栈问题:

一个栈的进栈序列为 1,2,3,..n,求不同的出栈序列有多少种

(5)路径问题:

在 n*n 的方格地图中,从一个角到另外一个角,求不跨越对角线的路径数有多少种

(6)握手问题:

2n 个人均匀坐在一个圆桌边上,某个时刻所有人同时与另一个人握手,要求手之间不能交叉,求共有多少种握手方法
接下来,我们来讲讲有关卡特兰数,也就是本题的具体处理。首先,我想到的是直接用计算机模拟栈的操作。

一、直接模拟

时限般 1 秒,这题如果你直接用栈来模拟太费时!(我算到 n=50 时就 2 秒了,结果很大:
1978261657756160653623774456)
而这里 n=50,离我们预期的目标 n=60000 还有很长的路,因此,我们后面肯定还需用高精度来做数据。

#include <iostream>
#include <cstdio>
#include <stack>
#include <vector>
using namespace std;
stack<int> sta;
vector<int> ans;
int n,num=0;
void dfs(int x) {
	if(x>=n&&sta.empty()) {
		num++;
		for(int i=0; i<ans.size(); ++i) printf("%d",ans[i]);
		printf("\n");
		return;
	}
	if(!sta.empty()) {
		int t=sta.top();
		ans.push_back(t);
		dfs(x);
		sta.pop();
		ans.pop_back();
	}
	if(x<=n) {
		sta.push(x);
		dfs(x+1);
		sta.pop();
	}
}
int main() {
	scanf("%d",&n);
	dfs(1);
	printf("%d\n",num);
	return 0;
}

分析 :

我们先将问题图形化,向右水平方向表示入栈,向上垂直方向表示出栈,根据题意,无论何时,入栈的次数均不少于出栈的次数,因此从 A 点到达 B 点,在绿色线内的所有蓝点都是合法方案数,而我们要找到问题解就在绿色直线红点上,这就是卡特兰数问题模型,标记的数值描述了几何图形含义。

A_zjzj

类似问题 1:

有 2n 个人排成一行进入剧场。入场费 5 元。其中只有 n 个人有一张 5 元钞票,另外 n 人只有 10 元钞票, 剧院无其它钞票,问有多少中方法使得只要有 10 元的人买票,售票处就有 5 元的钞票找零?(将持 5 元者到达视作将 5 元入栈,持 10 元者到达视作使栈中某 5 元出栈)

类似问题 2:

一位大城市的律师在他住所以北 n 个街区和以东 n 个街区处工作,每天她走 2n 个街区去上班。如果他从不穿越(但可以碰到)从家到办公室的对角线,那么有多少条可能的道路?

回到前面问题,对于每一个数来说,必须进栈一次、出栈一次。我们把进栈设为状态‘1’,出栈设为状态‘0’。 n个数的所有状态对应 n 个 1 和 n 个 0 组成的 2n 位二进制数。由于等待入栈的操作数按照 1‥n 的顺序排列、入栈的操作数 b 大于等于出栈的操作数 a(a≤b),因此输出序列的总数目=由左而右扫描由 n 个 1 和 n 个 0 组成的2n 位二进制数,1 的累计数不小于 0 的累计数的方案种数。
下面这个图形漂亮又直观(但不是我做的,百度来的)。

A_zjzj

二、动态规划初步
编程最容易想到的就是动态规划,当我往格子里填数的时候,还没填完动态规划思想就产生了。
动态规划在处理问题过程中速度快的很大原因是,程序在运算过程中将我们处理的状态记录下来,并在此基 础上,直接一步到位推导出下一状态。因此,动态规划解题一般都具有较佳的时间复杂度。
但对于本题而言,一开数组就发懵了(n=60000),如果将程序运行中各种状态都记录下来,数组得开
(\(60000\times60000=3.6\times10^9\)),你想这么做,但计算机内存空间不允许!
下面代码只通过了 n<20 小数据,原因是卡特兰数增长速度快,炸了!因此,我们还得高精度处理!

#include <iostream>
#include <cstdio>
#define N 610
using namespace std;
long long a[N][N];
int n=19;
int main() {
	for(int i=0; i<=n; ++i) {
		a[i][0]=1;
		a[0][i]=0;
	}
	for(int j=1; j<=n; ++j) {
		for(int i=j; i<=n; ++i) {
			a[i][j]=a[i][j-1]+a[i-1][j];
		}
	}
	printf("%d\n",a[n][n]);
	return 0;
}

三、滚动数组优化动态规划
下面再提供的改良方案,只记录与当前状态需要的前一种状态,这里我采用滚动数组来节省空间。

#include <iostream>
#include <cstdio>
#define N 600010
using namespace std;
long long a[2][N]= {0};
int n,t=0;
int main() {
	for(int i=0; i<=n; ++i) a[t][i]=1;
	for(int j=1; j<=n; ++j) {
		t=1-t; //用变量 t 来实现两个数组滚动
		a[t][j-1]=0; //重新前面这个位置值 0,因为这个位置曾用过有数值在
		for(int i=j; i<=n; ++i) {
			a[t][i]=a[1-t][i]+a[t][i-1];
			printf("%lld ",a[t][i]);
		}
		printf("\n");
	}
	return 0;
}

卡特兰数的几点补充:

1.给顶节点组成二叉树的问题
给定 N 个节点,能构成多少种形状不同的二叉树?
(一定是二叉树!先取一个点作为顶点,然后左边依次可以取 0 至 N-1 个相对应的,右边是 N-1 到 0 个,两两配对相乘,就是 h(0)h(n-1) + h(2)h(n-2) + + h(n-1)h(0)=h(n)) (能构成 h(N)个)
在 2n 位二进制数中填入 n 个 1 的方案数为 c(2n,n),不填 1 的其余 n 位自动填 0。从中减去不符合要求(由左而右扫描,0 的累计数大于 1 的累计数)的方案数即为所求。
不符合要求的数的特征是由左而右扫描时,必然在某一奇数位 2m+1 位上首先出现 m+1 个 0 的累计数和 m 个 1 的累计数,此后的 2(n-m)-1 位上有 n-m 个 1 和 n-m-1 个 0。
如若把后面这 2(n-m)-1 位上的 0 和 1 互换,使之成为 n-m 个 0 和 n-m-1 个 1,结果得 1 个由 n+1 个 0 和 n-1 个 1 组成的 2n 位数,即一个不合要求的数对应于一个由 n+1 个 0 和 n-1 个 1 组成的排列。
反过来,任何一个由 n+1 个 0 和 n-1 个 1 组成的 2n 位二进制数,由于 0 的个数多 2 个,2n 为偶数,故必在某一个奇数位上出现 0 的累计数超过 1 的累计数。同样在后面部分 0 和 1 互换,使之成为由 n 个 0 和 n 个 1 组成的 2n 位数,即 n+1 个 0 和 n-1 个 1 组成的 2n 位数必对应一个不符合要求的数。
因而不合要求的 2n 位数与 n+1 个 0,n-1 个 1 组成的排列一一对应。
显然,不符合要求的方案数为 c(2n,n+1)。由此得出输出序列的总数目=c(2n,n)-c(2n,n+1)=1/(n+1)*c(2n,n)。
(这个公式的下标是从 h(0)=1 开始的)

2.图形化卡特兰数证明 2(折线法)
和我前面给的图像证明如出一辙,下面这个处理同样非常有意思。

事实上,可以认为问题是,任意两种操作,要求每种操作的总次数一样,且进行第 k 次操作 2 前必须先进行至少 k 次操作 1。我们假设一个人在原点,操作 1 是此人沿右上角 45°走一个单位(一个单位设为根号 2,这样他第一次进行操作 1 就刚好走到(1,1)点),操作 2 是此人沿右下角 45°走一个单位。
第 k 次操作 2 前必须先进行至少 k 次操作 1,就是说明所走出来的折线不能跨越 x 轴走到 y=-1 这条线上! 在进行 n 次操作 1 和 n 此操作 2 后,此人必将到到达(2n,0)!若无跨越 x 轴的限制,折线的种数将为 C(2n,
n),即在 2n 次操作中选出 n 次作为操作 1 的方法数。

现在只要减去跨越了 x 轴的情况数。对于任意跨越 x 轴的情况,必有将与 y=-1 相交。找出第一个与 y=-1 相交的点 k,将k 点以右的折线根据 y=-1 对称(即操作 1 与操作 2 互换了)。可以发现终点最终都会从(2n,0)对称到(2n,-2)。由于对称总是能进行的,且是可逆的。

我们可以得出所有跨越了 x 轴的折线总数是与从(0,0)到
(2n,-2)的折线总数。
而后者的操作 2 比操作 1 要多 0-(-2)=2 次。即操作 1 为
n-1,操作 2 为 n+1。总数为 C(2n,n-1)。

以上就是卡特兰数的内容

若有错误,可以评论

谢谢--zhengjun

posted @ 2022-06-10 19:13  A_zjzj  阅读(110)  评论(0编辑  收藏  举报